Android - Ruby on Rails Project Part 2: Some simple SAX parsing
Must….Finish…..Post…
I’ve been sitting on this post for awhile. I had the code written weeks ago, but I’ve been procrastinating about getting the surrounding text completed. As I’ve been trying to get the text written I’ve come up with other ideas, and instead of continually modify the code, I’ll present one solution here, and in my next post I will address one that eliminates the Project POJO.
The goal of this post is to take the nodes of the retrieved XML file from our REST call that I discussed in my early blog post and convert it into an array of concrete objects holding the XML node values as attributes.
This will be done by the following classes and interfaces
1) IObjectTranslator
2) Project
3) AbstractTranslator
3) SaxParser
The methodology discussed is one of many ways to extract objects from an XML document. And just as a disclaimer to myself, this won’t be any ground breaking stuff, and being a Java programmer who usually has at least desktop processor and memory at his disposal, this may not be the most efficient solution for parsing an XML document on a mobile device, but nested if’s upon nest if’s are a pain to maintain in my most humble of opinions. Okay, enough futzing around.
This design utilizes the static parse method in the android.util.Xml class from Googles Android API. The DefaultHandler, which handles the callbacks from the SAX parser, is given the path of the node containing the desired object in the XML document. A Translator object, supplied with the class defining the object to create, extracts the desired elements from the node specified by the path. An example may explain it much better. Given the following XML snippet:
<projects>
<project>
<id>123</id>
<name>Project1</name>
</project>
</projects>
And the path “/projects/project”, the DefaultHandler hands the characters within the elements, “id” and “name” to the Translator to fill in the appropriate attributes of an instantiated Project object. This design could be extended to build many different object types from an object Factory supplied to the DefaultHandler, but for this simple program I didn’t want to go that far.
First, I’ll introduce an interface which is used to define the methods for translating objects defined by the XML.
package com.mgm.phobos;
public interface IObjectTranslator {
public void constructObject();
public boolean acceptingAttributes();
public void storeObject();
public Object[] getObjectList();
public void setAttribute( String name, String value );
}
For each class that the developer wishes to pull from the XML, another IObjectTranslator is supplied to define how the XML is translated into objects. In my example, only one translator is written which is for the Project POJO. The Project class follows, this simple example will extract the id and name nodes from the XML and fill in the Projects corresponding attributes.
package com.mgm.phobos;
public class Project
{
private int _id;
private String _name;
public void setName( String name )
{
_name = name;
}
public void setId( int id )
{
_id = id;
}
public String getName()
{
return _name;
}
public int getId()
{
return _id;
}
}
Next comes the AbstractTranslator which implements the boiler plate code handling many of the SAX callbacks. The most interesting part is that it uses the Class object to instantiate the Project POJO given the classes name.
package com.mgm.phobos;
import java.util.ArrayList;
import android.util.Log;
public abstract class AbstractTranslator implements IObjectTranslator
{
private ArrayList _objects = new ArrayList();
private String _className;
protected Object _currentObject = null;
public AbstractTranslator( String className )
{
_className = className;
}
/**
* Instantiate a new object defined by _className.
*/
public void constructObject()
{
if ( _currentObject != null )
{
_objects.add ( _currentObject );
}
try
{
Class theClass = Class.forName( _className );
_currentObject = theClass.newInstance();
Log.i("handler", "constructed object from class " + _className );
}
catch ( ClassNotFoundException ex )
{
System.err.println( ex + " Interpreter class must be in class path.");
}
catch( InstantiationException ex )
{
System.err.println( ex + " Interpreter class must be concrete.");
}
catch( IllegalAccessException ex )
{
System.err.println( ex + " Interpreter class must have a no-arg constructor.");
}
}
/**
* Determines if we have instantiated an object and are waiting for
* it's attributes
*/
public boolean acceptingAttributes()
{
return _currentObject != null;
}
/**
* The data has been loaded into _currentObject, store it.
*/
public void storeObject()
{
if ( _currentObject != null )
{
_objects.add ( _currentObject );
}
}
/**
* Retrieve all the objects loaded from the XML document
*/
public Object[] getObjectList()
{
return _objects.toArray ( );
}
}
The SaxParser defines a Translator specific to the Project POJO and the DefaultHandler to parse the XML document. The important thing here is that the only thing that changes with different XML documents is the definition of the AbstractTranslator supplied to the Document Handler. The removes the rewriting of the boiler plate handler code.
package com.mgm.phobos;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import android.util.Log;
import android.util.Xml;
public class SaxParser
{
IObjectTranslator _translator;
public void parseDoc( String doc )
{
try
{
_translator = new AbstractTranslator( "com.mgm.phobos.Project" )
{
// handle the "name" and "id" attribute specific to the Project object
public void setAttribute ( String name, String value )
{
if ( name != null )
{
if ( name.trim().equals ( "name" ) )
{
Log.d("attribute", "set name to " + value );
((Project)_currentObject).setName ( value.trim() );
}
else if ( name.trim().equals ( "id" ) )
{
Log.d("attribute", "set id to " + value );
((Project)_currentObject).setId ( Integer.parseInt ( value.trim() ) );
}
} // if name != null
} // setAttribute
};
// instantiate the handler along with the path we are interested in for the
// _translator object
DocHandler handler = new DocHandler( "/projects/project", _translator );
Xml.parse(doc, handler);
}
catch ( SAXException e )
{
throw new RuntimeException( e );
}
}
public Object[] getObjectList()
{
return _translator.getObjectList ( );
}
/**
* A Handler for the XML document to define the callbacks to the specified translator
*/
class DocHandler extends DefaultHandler
{
private StringBuffer _currentPath;
private String _objectPath;
private IObjectTranslator _translator;
private String _currentAttribute;
public DocHandler( String objectPath, IObjectTranslator translator )
{
_objectPath = objectPath;
_translator = translator;
_currentPath = new StringBuffer();;
}
public void characters ( char[] ch, int start, int length )
throws SAXException
{
String stg = new String( ch , start, length );
// ensure that if another path has a similar attribute it isn't picked up
if ( _translator.acceptingAttributes ( ) )
{
_translator.setAttribute ( _currentAttribute, stg );
}
}
public void endElement ( String uri, String localName, String name )
throws SAXException
{
Log.i("handler", "end " + localName);
// clean up at the end of the element node
if ( _currentPath.length ( ) > 0 && localName.trim().length ( ) > 0 )
{
String path = "/" + localName;
_currentPath.delete ( _currentPath.lastIndexOf ( path ), _currentPath.length ( ) );
_currentAttribute = "";
}
}
public void startElement ( String uri,
String localName,
String name,
Attributes attributes )
throws SAXException
{
_currentPath.append( "/" + localName );
if ( _currentPath.toString ( ).equals ( _objectPath ) )
{
// we've arrived at a path defining one of our objects
// for our Project POJO the path is "/projects/project"
_translator.constructObject ( );
}
else if ( _translator.acceptingAttributes ( ) )
{
// we have instantiated an object due to the defined path,
// save this attribute as one we may be interested in
// for the Project example, the attributes we are interested
// in are "name" and "id"
Log.i("handler", "accept attribute " + localName );
_currentAttribute = localName;
}
}
public void endDocument ( ) throws SAXException
{
_translator.storeObject ( );
}
}
}
Finally is the call from the Phobos Activity (defined in the early blog post) class to the SaxParser. This call is made from within the retreiveProjects method
private void retreiveProjects()
{
HttpClient httpClient = new DefaultHttpClient();
try
{
String url = "http://10.0.1.2:3000/projects?format=xml";
Log.d( "phobos", "performing get " + url );
HttpGet method = new HttpGet( new URI(url) );
HttpResponse response = httpClient.execute(method);
if ( response != null )
{
String result = getResponse(response.getEntity());
Log.i( "phobos", "received " + result );
// parse the XML document and build the Project objects
SaxParser parser = new SaxParser();
parser.parseDoc ( result );
StringBuilder builder = new StringBuilder();
Object[] projects = parser.getObjectList();
Project project;
Log.i( "xml", "found " + projects.length );
for( int idx = 0; idx < projects.length; idx++ )
{
project = (Project)projects[idx];
builder.append( "Project: id: " + project.getId ( ) + " name: " + project.getName ( ) + "\n" );
}
// bare bones output of parsed objects
tv.setText(builder.toString());
}
else
{
Log.i( "phobos", "got a null response" );
}
} catch (IOException e) {
Log.e( "ouch", "!!! IOException " + e.getMessage() );
} catch (URISyntaxException e) {
Log.e( "ouch", "!!! URISyntaxException " + e.getMessage() );
}
}
Phew…I finally got it all documented, this is still a learning process for me, so sorry if it’s a little terse in some sections. The main thing I wanted to demo was the ease of use that the Android API makes SAX parsing with the simple call to the Xml.parse(…) method suppling the XML document and the Default Handler. Also, by extrapolating the retrieval of the Project’s attributes into the IObjectTranslator, this is the only code that is changed if more attributes are added or removed, or if a completely different object is retrieved from the XML document. Obviously this only defines one POJO, and as stated above, a Factory could be supplied to the Default Handler to handle more then one POJO. In my next post on this subject I want to show an example where the POJO isn’t needed, but that code has yet to be written, hopefully this time I can remember to document as I go so writing up the blog post isn’t such a pain.

November 17th, 2008 at 7:12 pm
Hi Michael,
I am currently working on a project, that uses Android and Rails as you do. My goal is to write a web-application based on Rails (with RESTful service) that makes it possible for users to post information with their Android mobile phones.
But I have a problem, and maybe you can help me: users should have the possibility to add a picture to their post, unfortunately this doesn’t work so far. I have tried out many ways, but without success.
Have you got any hints or ideas how I can solve this problem? That would be great. If desired I can send you detailed informations like code-fragments,…
Thank you in advance!
Regards
Emrah
November 17th, 2008 at 10:53 pm
Hey Emrah,
I’m assuming you cannot POST a file. But the only time I’ve uploaded a file to a Rails website, I used the upload functionality provided by Rails. It doesn’t sound like you want to do that though. You may have to open a separate socket on the server to accept file uploads.
My only other words of advice would be to post a question on the Android Developer forum or take a look at the Downloader example from the Android Developer blog, maybe there a hint there on how to do the opposite uploading,
http://android-developers.blogspot.com/2008/09/three-new-samples-triangle-spritetext.html
The code can be seen here,
http://code.google.com/p/apps-for-android/source/browse/#svn/trunk/Samples/Downloader
Good luck, would love to hear if you have any success, maybe I’ll have to try it when I get some time.
Cheers,
michael
November 25th, 2008 at 12:16 am
[…] Part 2 - Sax Parsing […]
December 5th, 2008 at 3:49 am
Hi Michael!
I have solve it. There were many little faults and they lead to the problems :-). But especially the rails security option “protect_from_forgery” was the biggest “problem”.
Regards,
Emrah
December 11th, 2008 at 2:33 pm
[…] Part 2- SAX Parsing […]
December 12th, 2008 at 5:20 pm
Nice one again, Michael.
I have to ask though. How is it working with Java and XML, after you found out how programming could be, i.e. Ruby/Rails?
I am in a similar position and left Java mostly behind me until Android. And I have to say that I often feel suicidal when dealing with Java nowadays.
Hopefully there will be a Ruby to DEX compiler someday, or Groovy, or Python, or Lua, or JavaScript.
December 12th, 2008 at 6:08 pm
Mariano,
Thanks for the comment. I’ve been doing Java development for about 10 years now, and using the XML parsing APIs for about 6, so it come pretty naturally. I haven’t done a whole lot of XML parsing in Ruby and have only been doing side projects in RoR on and off for the last year or 2. I will state 100% though…IMHO… Ruby on Rails kicks butt on Java web development. I’ve been on a JBoss project for the last 6 months or so and it’s been very challenging learning curve when compared to the amount of effort it took to get going with Ruby on Rails. I understand that it’s not exactly comparing apples to apples, but getting my first prototype up and running in Rails was much faster then the equivalent work in a Java web environment.
I’ve actually been really enjoying using Java and Android. I believe Google has done a great first pass at a product/framework that gives mobile developers a great platform to launch their applications from.
cheers
- michael