Using ksoap2 for android, and parsing output data


SDK Version: 
M3

So the other day, I was asked to check out how we could use soap on Android, preferably with ksoap2 for android, and a public SOAP Web Service. For the latter the TopGoalScorers web service was chosen.

This example will prepare a soap message with one extra variable and value (iTopN, 5) and get a soap object as response.

  1. private static final String METHOD_NAME = "TopGoalScorers";
  2.  private static final String SOAP_ACTION = "http://footballpool.dataaccess.eu/data/TopGoalScorers";
  3.  private static final String NAMESPACE = "http://footballpool.dataaccess.eu";
  4.  private static final String URL = "http://footballpool.dataaccess.eu/data/info.wso?WSDL";
  5. //you can get these values from the wsdl file^
  6.  
  7.  public SoapObject soap(String METHOD_NAME, String SOAP_ACTION, String NAMESPACE, String URL) throws IOException, XmlPullParserException {
  8.     SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME); //set up request
  9.     request.addProperty("iTopN", "5"); //variable name, value. I got the variable name, from the wsdl file!
  10.     SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11); //put all required data into a soap envelope
  11.     envelope.setOutputSoapObject(request);  //prepare request
  12.     AndroidHttpTransport httpTransport = new AndroidHttpTransport(URL);  
  13.     httpTransport.debug = true;  //this is optional, use it if you don't want to use a packet sniffer to check what the sent message was (httpTransport.requestDump)
  14.     httpTransport.call(SOAP_ACTION, envelope); //send request
  15.     SoapObject result=(SoapObject)envelope.getResponse(); //get response
  16.     return result;
  17.  }
  18.  
  19. //usage:
  20. //SoapObject result=soap(METHOD_NAME, SOAP_ACTION, NAMESPACE, URL);
  21. //don't forget to catch the exceptions!!

So after I managed to get a proper soap response, it was time to parse it. I couldn't find any proper already made method to parse it, that actually worked.

You can get the soap objects elements, line by line like this...

  1. result.getProperty(elementNumber).toString();

...but, this is it! There is no getValue(), or anything like it that would even resemble a parser.

A response soap message from ksoap2 looks like this:

  1. anyType{tTopGoalScorer=anyType{sName=Gonzalo HiguaÃn; iGoals=3; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/ar.gif; }; tTopGoalScorer=anyType{sName=Asamoah Gyan; iGoals=2; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/gh.gif; }; tTopGoalScorer=anyType{sName=Brett Holman; iGoals=2; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/au.gif; }; tTopGoalScorer=anyType{sName=Chung-Yong Lee; iGoals=2; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/kr.gif; }; tTopGoalScorer=anyType{sName=David Villa; iGoals=2; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/es.gif; }; }

Pretty well structured, but a pain in the ass to work with.
The official documentation suggests a way to implement and register an object with the Marshal interface, but after seeing the string representation above, it seems to be way too complicated. Let's see how we could make things easier, and more importantly, let's try to achieve it in a generalized way.

This is one element:

  1. {sName=Gonzalo HiguaÃn; iGoals=3; sCountry=Y; sFlag=http://footballpool.dataaccess.eu/images/flags/ar.gif; }

Obviously I could make something like this fast, but this is only works for the example service.

  1. //parse the soap object, one element at a time.
  2.  public String lameParser(String input){
  3.     String sName=input.substring(input.indexOf("sName=")+6, input.indexOf(";", input.indexOf("sName=")));
  4.     int IGoals=Integer.valueOf(input.substring(input.indexOf("iGoals=")+7, input.indexOf(";", input.indexOf("iGoals="))));
  5.     String sCountry=input.substring(input.indexOf("sCountry=")+9, input.indexOf(";", input.indexOf("sCountry=")));
  6.     String sFlag=input.substring(input.indexOf("sFlag=")+6, input.indexOf(";", input.indexOf("sFlag=")));
  7.     return sName+" "+Integer.toString(IGoals)+" "+sCountry+" "+sFlag;
  8.  }

..but doing this every time, for every response message type, is just multiplying code, and too much work anyway.

After wondering a couple of minutes over the code, it looked as we could create a general method with reflection to do the job.
The method above receives one line of string from the result object, and a class, that has public members. To be able to map the SOAP response values with the corresponding members, the names and types of the member fields have to be exactly the same, as they are in the result response. As in the first example, we used the TopGoalScorers method, to test the parser, it had the following fields: sName, iGoals, sCountry, sFlag. For our web service example the result class (aka Business Object) is:

  1. package com.helloandroid.soaptest;
  2.  
  3. public class TopGoalScores {
  4.     String Name;
  5.     int Goals;
  6.     String Country;
  7.     String Flag;
  8. }

And here comes the parser implementation. Please note, that in the current form this only works with primitive types, like String, int, Integer, float and Float.

  1. package com.helloandroid.ksoap2;
  2.  
  3. import java.lang.reflect.Field;
  4. import java.lang.reflect.Type;
  5.  
  6. /**
  7.  * Ksoap2 for android - output parser
  8.  * This class parses an input soap message
  9.  * @author tamas.beres@helloandroid com, akos.birtha@helloandroid com
  10.  *
  11.  */
  12. public class Ksoap2ResultParser {
  13.  
  14.     /**
  15.     * Parses a single business object containing primitive types from the response
  16.     * @param input soap message, one element at a time
  17.     * @param theClass your class object, that contains the same member names and types for the response soap object
  18.     * @return the values parsed
  19.     * @throws NumberFormatException
  20.     * @throws IllegalArgumentException
  21.     * @throws IllegalAccessException
  22.     * @throws InstantiationException
  23.     */
  24.     public static void parseBusinessObject(String input, Object output) throws NumberFormatException, IllegalArgumentException, IllegalAccessException, InstantiationException{
  25.  
  26.         Class theClass = output.getClass();
  27.         Field[] fields = theClass.getDeclaredFields();
  28.        
  29.         for (int i = 0; i < fields.length; i++) {
  30.             Type type=fields[i].getType();
  31.             fields[i].setAccessible(true);
  32.  
  33.             //detect String
  34.             if (fields[i].getType().equals(String.class)) {
  35.                 String tag = "s" + fields[i].getName() + "=";   //"s" is for String in the above soap response example + field name for example Name = "sName"
  36.                 if(input.contains(tag)){
  37.                     String strValue = input.substring(input.indexOf(tag)+tag.length(), input.indexOf(";", input.indexOf(tag)));
  38.                     if(strValue.length()!=0){
  39.                         fields[i].set(output, strValue);
  40.                     }
  41.                 }
  42.             }
  43.  
  44.             //detect int or Integer
  45.             if (type.equals(Integer.TYPE) || type.equals(Integer.class)) {
  46.                 String tag = "i" + fields[i].getName() + "=";  //"i" is for Integer or int in the above soap response example+ field name for example Goals = "iGoals"
  47.                 if(input.contains(tag)){
  48.                     String strValue = input.substring(input.indexOf(tag)+tag.length(), input.indexOf(";", input.indexOf(tag)));
  49.                     if(strValue.length()!=0){
  50.                         fields[i].setInt(output, Integer.valueOf(strValue));
  51.                     }
  52.                 }
  53.             }
  54.  
  55.             //detect float or Float
  56.             if (type.equals(Float.TYPE) || type.equals(Float.class)) {
  57.                 String tag = "f" + fields[i].getName() + "=";
  58.                 if(input.contains(tag)){
  59.                     String strValue = input.substring(input.indexOf(tag)+tag.length(), input.indexOf(";", input.indexOf(tag)));
  60.                     if(strValue.length()!=0){
  61.                         fields[i].setFloat(output, Float.valueOf(strValue));
  62.                     }
  63.                 }
  64.             }
  65.         }
  66.        
  67.     }
  68. }

Now this class can be easily reused for parsing any response with primitive types, and it provides the result in a convenient form to work with.
Usage:

  1.  TopGoalScores topGoalScores=new TopGoalScores();
  2.  
  3.  try {
  4.     Ksoap2ResultParser.parseBusinessObject(soapresultmsg.getProperty(0).toString(), topGoalScores);
  5.  
  6.  } catch (NumberFormatException e) {
  7.     // TODO Auto-generated catch block
  8.     e.printStackTrace();
  9.  } catch (IllegalArgumentException e) {
  10.     // TODO Auto-generated catch block
  11.     e.printStackTrace();
  12.  } catch (IllegalAccessException e) {
  13.     // TODO Auto-generated catch block
  14.     e.printStackTrace();
  15.  } catch (InstantiationException e) {
  16.     // TODO Auto-generated catch block
  17.     e.printStackTrace();
  18.  }

We know, that there is still room for improvements. The error reporting could be done in a sophisticated way for example. Or maybe we could do an implementation, that not only parses one object, but a list of objects. With this article we just wanted to introduce the basic idea.