Android Lists V: Accessing and Consuming a SOAP Web Service II
Continued from: Android Lists IV: Accessing and Consuming a SOAP Web Service I
When we left off, we were able to see the result XML that was retrieved from the SOAP web service that was called with the StockQuoteFetcher. Now it’s time to parse that XML data into Java objects, so that the data can be used within the StockViewer application.
The result of the web service that was chosen in the previous article actually just returns a String of XML, like this:
<?xml version="1.0" encoding="utf-8"?> <string xmlns="http://www.webserviceX.NET/"> <StockQuotes> <Stock> <Symbol>MSFT</Symbol> <Last>25.92</Last> <Date>4/29/2011</Date> <Time>4:00pm</Time> <Change>-0.79</Change> <Open>26.55</Open> <High>26.64</High> <Low>25.36</Low> <Volume>319317856</Volume> <MktCap>217.8B</MktCap> <PreviousClose>26.71</PreviousClose> <PercentageChange>-2.96%</PercentageChange> <AnnRange>22.73-31.08</AnnRange> <Earns>2.343</Earns> <P-E>11.40</P-E> <Name>Microsoft Corpora</Name> </Stock> </StockQuotes> </string>
This makes more conventional methods of retrieving a structured result a little more difficult since the result is just an XML string as opposed to an XML result that is structured. That means that the XML data will need to be pulled from the web service response, and then that XML data will need to be parsed to create the Java objects that can be used in the Stock Viewer application.
XML Parsing Options
One of the many nice features of Android is that there is more than one way to go about parsing an XML document.
SAX Parser
The SAX parser is entirely the same in Android as it is in Java, which is cool because if you have a parser for a Java application, you can use it directly in an Android application.
Advantages
- It’s quick. SAX is event based and traverses the XML document from start to end
- Small memory footprint
Disadvantages
- Somewhat tedious to develop and maintain
- Restricted to only traversing the XML document from start to finish a single time
- Intended for forward, read-only operations
DOM Parser
Advantages
- Ability to add/remove/edit elements of the XML document via the Document Object Model
- Visit single elements multiple times without having to re-parse the document
- Ability to access elements in an arbitrary sequence
Disadvantages
- Entire document is loaded into memory
- Slowest parsing method of all three
Pull Parser
Advantages
- Ability to “pull” data from the XML document, as opposed to being pushed the data (via callbacks) when using SAX
- Small memory footprint
Disadvantages
- Slower than SAX
- Restricted to only traversing the XML document from start to finish a single time
- Intended for forward, read-only operations
Parsing the SOAP Web Service Response
To make it a bit clearer, the XML that is extracted from the web service result looks something like this (a little bit easier to read than the encoded < and > symbols up above):
<StockQuotes> <Stock> <Symbol>MSFT</Symbol> <Last>25.92</Last> <Date>4/29/2011</Date> <Time>4:00pm</Time> <Change>-0.79</Change> <Open>26.55</Open> <High>26.64</High> <Low>25.36</Low> <Volume>319317856</Volume> <MktCap>217.8B</MktCap> <PreviousClose>26.71</PreviousClose> <PercentageChange>-2.96%</PercentageChange> <AnnRange>22.73 - 31.08</AnnRange> <Earns>2.343</Earns> <P-E>11.40</P-E> <Name>Microsoft Corpora</Name> </Stock> </StockQuotes>
For this section, I have opted to go with SAX. There are two steps that need to be accomplished in order to retrieve a listing of StockQuote from the XML result string.
SAX Handler
First, we need to create a SAX handler, which will be a class that extends the DefaultHandler class that is provided in the SAX namespace. This handler is essentially the logic for how the data in the XML document is mapped to properties of the StockQuote object.
package com.austinrasmussen.stockviewer; import java.util.ArrayList; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class StockQuoteHandler extends DefaultHandler { private ArrayList<StockQuote> quotes = new ArrayList<StockQuote>(); private StockQuote currentQuote = new StockQuote(); private String currentNodeText; private final String STOCK = "Stock"; private final String SYMBOL = "Symbol"; private final String LAST = "Last"; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // Create a new StockQuote for every corresponding // <Stock> node in the XML document if (localName.equalsIgnoreCase(STOCK)) { currentQuote = new StockQuote(); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { // Retrieve the text content of the current node // that is being processed currentNodeText = new String(ch, start, length); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if(localName.equalsIgnoreCase(SYMBOL)){ currentQuote.setTickerSymbol(currentNodeText); }else if(localName.equalsIgnoreCase(LAST)){ currentQuote.setQuote(Double.parseDouble(currentNodeText)); }else if(localName.equalsIgnoreCase(STOCK)){ // When the </Stock> element is reached, this quote object is complete. quotes.add(currentQuote); } } public ArrayList<StockQuote> getQuotes() { return quotes; } }
The handler, while having many conditional statements, is quite straightforward how the data is mapped from the XML element names to the properties of the StockQuote object. Here’s a synopsis of how this handler works:
- The constants at the top define the names of the XML elements that the Stock Viewer application is interested in.
- The startElement(…) method is invoked every time a start element of the XML document is encountered, such as <Stock> or <Symbol>, but not </Stock> or </Symbol>.
- The characters(…) method is invoked every time character data inside of the current XML element is encountered, such as MSFT in <Stock>MSFT</Stock>.
- The endElement(…) method is invoked every time an end element of the XML document is encountered, such as </Stock> or </Symbol>, but not <Stock> or <Symbol>.
- Most of the interesting operations happen within this method.
- This is when the handler knows that all of the data for this particular element can be read in and assigned to a property (since the characters(…) method would have been invoked before this method).
- This is also when we might possibly encounter the </Stock> element, which means that the current StockQuote object is fully populated.
Using the Handler
Now that the handler is created, all that is left to do is use it to process the web service response XML. There’s a handy Xml(…) method that Android provides in the android.util namespace that will assist in performing this operation.
Let’s revisit the Fetch() method of StockQuoteFetcher… Right now, it looks something like this:
public String Fetch() { String result = ""; HttpTransportSE httpRequest = new HttpTransportSE(URL); try { httpRequest.call(SOAP_ACTION, envelope); SoapPrimitive response = (SoapPrimitive)envelope.getResponse(); result = response.toString(); } catch(Exception e) { e.printStackTrace(); } return result; }
With just a little bit of modification and the Xml(…) method, the Fetch() method is able to return a List of StockQuote objects to the Activity class.
public List<StockQuote> Fetch() { HttpTransportSE httpRequest = new HttpTransportSE(URL); StockQuoteHandler quoteParser = new StockQuoteHandler();; try { httpRequest.call(SOAP_ACTION, envelope); SoapPrimitive response = (SoapPrimitive)envelope.getResponse(); Xml.parse(response.toString(), quoteParser); } catch(Exception e) { e.printStackTrace(); } return quoteParser.getQuotes(); }
Fantastic! That was simple enough. We can now replace the static data set in the main Activity class with the result of the Fetch(…) method.
package com.austinrasmussen.stockviewer; import java.util.List; import android.app.ListActivity; import android.os.Bundle; public class StockList extends ListActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); StockQuoteFetcher sqf = new StockQuoteFetcher("MSFT ORCL AMZN ERTS"); List<StockQuote> quoteResult = sqf.Fetch(); setListAdapter(new StockQuoteAdapter(this, quoteResult)); } }
Results
Let’s see how the Stock Viewer application works with the newly added SAX handler that has been added to parse the web service response.
Excellent! Just as expected, the data was parsed into StockQuote objects successfully by the StockQuoteHandler. The resulting list of StockQuote objects was then used as the data source for the StockQuoteAdapter instead of the static content that had been used previously.
Final Code
package com.austinrasmussen.stockviewer; import java.util.List; import android.app.ListActivity; import android.os.Bundle; public class StockList extends ListActivity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); StockQuoteFetcher sqf = new StockQuoteFetcher("MSFT ORCL AMZN ERTS"); List<StockQuote> quoteResult = sqf.Fetch(); setListAdapter(new StockQuoteAdapter(this, quoteResult)); } }
package com.austinrasmussen.stockviewer; public class StockQuote { private String tickerSymbol; private Double quote; public StockQuote() { } public StockQuote(String tickerSymbol, Double quote) { this.tickerSymbol = tickerSymbol; this.quote = quote; } public void setTickerSymbol(String tickerSymbol) { this.tickerSymbol = tickerSymbol; } public String getTickerSymbol() { return tickerSymbol; } public void setQuote(double quote) { this.quote = quote; } public Double getQuote() { return quote; } }
package com.austinrasmussen.stockviewer; import java.util.List; import android.app.Activity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; public class StockQuoteAdapter extends ArrayAdapter<StockQuote> { private final Activity activity; private final List<StockQuote> stocks; public StockQuoteAdapter(Activity activity, List<StockQuote> objects) { super(activity, R.layout.stock_quote_list_item , objects); this.activity = activity; this.stocks = objects; } @Override public View getView(int position, View convertView, ViewGroup parent) { View rowView = convertView; StockQuoteView sqView = null; if(rowView == null) { // Get a new instance of the row layout view LayoutInflater inflater = activity.getLayoutInflater(); rowView = inflater.inflate(R.layout.stock_quote_list_item, null); // Hold the view objects in an object, // so they don't need to be re-fetched sqView = new StockQuoteView(); sqView.ticker = (TextView) rowView.findViewById(R.id.ticker_symbol); sqView.quote = (TextView) rowView.findViewById(R.id.ticker_price); // Cache the view objects in the tag, // so they can be re-accessed later rowView.setTag(sqView); } else { sqView = (StockQuoteView) rowView.getTag(); } // Transfer the stock data from the data object // to the view objects StockQuote currentStock = stocks.get(position); sqView.ticker.setText(currentStock.getTickerSymbol()); sqView.quote.setText(currentStock.getQuote().toString()); return rowView; } protected static class StockQuoteView { protected TextView ticker; protected TextView quote; } }
package com.austinrasmussen.stockviewer; import java.util.List; import org.ksoap2.SoapEnvelope; import org.ksoap2.serialization.PropertyInfo; import org.ksoap2.serialization.SoapObject; import org.ksoap2.serialization.SoapPrimitive; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.ksoap2.transport.AndroidHttpTransport; import org.ksoap2.transport.HttpTransportSE; import android.util.Xml; public class StockQuoteFetcher { private final String NAMESPACE = "http://www.webserviceX.NET/"; private final String METHOD_NAME = "GetQuote"; private final String SOAP_ACTION = "http://www.webserviceX.NET/GetQuote"; private final String URL = "http://www.webservicex.net/stockquote.asmx"; private final SoapSerializationEnvelope envelope; public StockQuoteFetcher(String quotes) { SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME); PropertyInfo quotesProperty = new PropertyInfo(); quotesProperty.setName("symbol"); quotesProperty.setValue(quotes); quotesProperty.setType(String.class); request.addProperty(quotesProperty); envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11); envelope.dotNet = true; envelope.setOutputSoapObject(request); } public List<StockQuote> Fetch() { HttpTransportSE httpRequest = new HttpTransportSE(URL); StockQuoteHandler quoteParser = new StockQuoteHandler();; try { httpRequest.call(SOAP_ACTION, envelope); SoapPrimitive response = (SoapPrimitive)envelope.getResponse(); Xml.parse(response.toString(), quoteParser); } catch(Exception e) { e.printStackTrace(); } return quoteParser.getQuotes(); } }
package com.austinrasmussen.stockviewer; import java.util.ArrayList; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; public class StockQuoteHandler extends DefaultHandler { private ArrayList<StockQuote> quotes = new ArrayList<StockQuote>(); private StockQuote currentQuote = new StockQuote(); private String currentNodeText; private final String STOCK = "Stock"; private final String SYMBOL = "Symbol"; private final String LAST = "Last"; @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // Create a new StockQuote for every corresponding // <Stock> node in the XML document if (localName.equalsIgnoreCase(STOCK)) { currentQuote = new StockQuote(); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { // Retrieve the text content of the current node // that is being processed currentNodeText = new String(ch, start, length); } @Override public void endElement(String uri, String localName, String qName) throws SAXException { if(localName.equalsIgnoreCase(SYMBOL)){ currentQuote.setTickerSymbol(currentNodeText); }else if(localName.equalsIgnoreCase(LAST)){ currentQuote.setQuote(Double.parseDouble(currentNodeText)); }else if(localName.equalsIgnoreCase(STOCK)){ // When the </Stock> element is reached, this quote object is complete. quotes.add(currentQuote); } } public ArrayList<StockQuote> getQuotes() { return quotes; } }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <ListView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@android:id/list" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/ticker_symbol" android:layout_alignParentLeft="true" android:textColor="@android:color/white" android:textSize="20sp" android:paddingTop="4dp" android:paddingBottom="4dp" /> <TextView android:layout_width="wrap_content" android:layout_height="fill_parent" android:id="@+id/ticker_price" android:layout_alignParentRight="true" android:layout_centerVertical="true" /> </RelativeLayout>
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.austinrasmussen.stockviewer" android:versionCode="1" android:versionName="1.0"> <uses-permission android:name="android.permission.INTERNET"></uses-permission> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".StockList" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
This entry was posted on Saturday, April 30th, 2011 at 1:07 pm and is filed under Android. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.
Austin Rasmussen May 23rd, 2012 at 6:46 pm
Anton,
I do provide the source, usually inline with the articles, but I understand that sometimes it may be difficult to piece together.
I started setting up a place where I’d publicly publish the finished product, but it appears that I never finished following through.
The completed StockViewer application can be found here: https://github.com/austin-rasmussen/vexed-logic/tree/master/StockViewer
Take a look through this and reply back if you’re still experiencing problems!