RESTful web services are currently all the rage. They’re the new black. Everybody’s doing it. Facebook, Twitter, and Amazon all have RESTful services. And if you’ve coded against one of these trendy APIs, you’ll probably turn your nose up at the mere mention of something like SOAP. “That’s SOOO last decade.” Perhaps. But the fact remains that there are a lot of web services still using SOAP technologies. And if you’re a mobile developer working on web service clients, you’re bound to bump into a SOAP server at some point.
Unfortunately for Android developers, the current platform SDK provides no built-in support for SOAP. The developer is left to either roll their own solution or search for something from a third party. And as fun as writing your own SOAP library sounds (not), coding and testing takes times. So a custom SOAP solution might not even be practical. As luck would have it, though, there’s a third party SOAP library for Android that’s mature, flexible, open-source, and best of all, completely free. It’s called ksoap2-android.
Introducing ksoap2-android
ksoap2-android is a quite a mouthful. It wasn’t always called ksoap2-android, though. And it wasn’t always an Android library. ksoap2-android started out as simply kSOAP way back in 2001. kSOAP targeted embedded J2ME systems. So it was designed with resource constrained environments in mind. After a number of small releases and a major refactor, it was then dubbed kSOAP2 and subsequently started dying on the vine. The maintainers stopped maintaining it. New releases grounded to a halt.
Fast forward to 2007. An Android developer by the name of Jorge Jimenez published an Android-specific set of patches for kSOAP2 to the “Android Developers” Google Group. Not long after ksoap2-android was born. A new home was created for the fork (http://code.google.com/p/ksoap2-android/). A new mailing list and wiki were erected. And ksoap2-android has since become THE library for SOAP on Android.
As of this writing, the latest stable release of ksoap2-android is 2.6.0.
Getting Started with ksoap2-android
This article is meant to be an introduction to ksoap2-android and not SOAP itself. If you’re not familiar with the inner-workings of SOAP, I’ll give a brief overview of it below. But I strongly advise you to bookmark this article and come back to it after having dived into a meatier tutorial on the subject.
You can obtain the ksoap2-android library one of two ways. If you prefer to build from source, you can find source download and build instructions here.
http://code.google.com/p/ksoap2-android/wiki/SourceCodeHosting
But the easiest way to get ksoap2-android is to fetch the latest pre-built JAR file. Instructions and links for downloading the JAR can be found here.
http://code.google.com/p/ksoap2-android/wiki/HowToUse?tm=2
A SOAP Primer
SOAP clients and servers communicate with each other using SOAP messages over some network transport protocol, which is usually HTTP. A SOAP message is merely a blob of XML and it can represent either a request or a response.
Here’s a small example that illustrates what a typical SOAP message might look like. Note that this is a contrived example and doesn’t represent a real service request.
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<soap:Header>
</soap:Header>
<soap:Body>
<example:FindBookByISBN xmlns:example="http://services.example.com">
<example:ISBN xsi:type="xsd:string">0-374-15012-5</example:ISBN>
</example:FindBookByISBN>
</soap:Body>
</soap:Envelope> |
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<soap:Header>
</soap:Header>
<soap:Body>
<example:FindBookByISBN xmlns:example="http://services.example.com">
<example:ISBN xsi:type="xsd:string">0-374-15012-5</example:ISBN>
</example:FindBookByISBN>
</soap:Body>
</soap:Envelope>
The structure of a SOAP message is rigidly defined and is comprised of three major pieces.
- The first piece is the SOAP envelope. This is essentially the container for all of the other SOAP data. The XML element name is Envelope and it’s always the root element.
- The second piece of a SOAP message is the SOAP header. This contains application-specific metadata relating to the request, such as encoding style, authentication information, etc. It’s considered optional and, in my experience, isn’t used all that often. The XML element name is Header and it’s a direct child of the Envelope element.
- The final, and most significant, part of a SOAP message is the SOAP body. This contains the actual message request or response. The content of the SOAP body is application dependent and can be anything as long as its well-formed XML. The XML element name is Body and it’s also a direct child of the Envelope element. The data types used in the SOAP body are often defined in a service type definition such as a WSDL or WADL file.
One other SOAP element bears mentioning and that’s the SOAP Fault. You can only ever have one SOAP Fault and it appears in the SOAP body. SOAP Faults are used to indicate error conditions. If, for example, you’re trying to communicate with a web service that doesn’t understand the version of SOAP you’re using, it’ll respond with a SOAP Fault that indicates such. The XML element name is Fault.
For more information on SOAP, poke around on Google or check out one of the dozens of books on SOAP web services.
Speaking SOAP using ksoap2-android
If you’ve worked with other web service SDKs (e.g., .NET), you’ve probably used tools that can generate language type definitions from a web service definition file (e.g. WSDL). These kinds of tools abstract away a lot of the “SOAPiness” of web service communications. They keep your application code simple and easy to maintain. I’m sorry to tell you that, at the time of this writing, no such tool for ksoap2-android exists. You’ll have to get a bit more up close and personal with whatever service description your web service provides.
Of the dozens of classes defined in ksoap2-android, there are only five that you’ll typically use – SoapPrimitive, SoapObject, SoapSerializationEnvelope, HttpTransportSE, and SoapFault. An understanding of these classes is all you need to perform a very simple SOAP request. I’ll discuss each one in turn and show you how you could invoke the “FindBookByISBN” SOAP request in the example above.
SoapPrimitive
A SoapPrimitive is just what it sounds like. It represents a serialized representation of a primitive data type in SOAP (floats, ints, doubles, strings, etc.) A SoapPrimitive doesn’t actually contain any type information, though. Everything is stored as strings. It’s up to the application developer to interpret and parse the contained data as appropriate.
SoapPrimitive’s constructor looks like this.
public SoapPrimitive(String namespace, String name, String value); |
public SoapPrimitive(String namespace, String name, String value);
You usually won’t instantiate this yourself. In general, SOAP requests are more complex and require the use of the SoapObject, which I’ll discuss next. But it’s not uncommon to receive this as a response.
SoapObject
The SoapObject represents a complex type that’s defined for a given web service. These types are hierarchal data types composed of other data types and live in an application-specific namespace. The FindBookByISBN element used above is an example of a complex type.
<example:FindBookByISBN xmlns:example="http://services.example.com">
<example:ISBN xsi:type="xsd:string">0-374-15012-5</example:ISBN>
</example:FindBookByISBN> |
<example:FindBookByISBN xmlns:example="http://services.example.com">
<example:ISBN xsi:type="xsd:string">0-374-15012-5</example:ISBN>
</example:FindBookByISBN>
SoapObject’s constructor looks like this.
public SoapObject(String namespace, String name); |
public SoapObject(String namespace, String name);
You can instantiate a SoapObject by passing in the object’s namespace and name into the constructor.
SoapObject findBookRequest = new SoapObject(
"http://services.example.com", "FindBookByISBN"); |
SoapObject findBookRequest = new SoapObject(
"http://services.example.com", "FindBookByISBN");
The FindBookByISBN method has a single parameter, ISBN. ISBN is a simple string. You might expect that you could simply wrap the ISBN inside of a SoapPrimitive and somehow add it into findBookRequest. That would be a perfectly reasonable approach. For some reason, however, SoapObject isn’t able to serve as a container for SoapPrimitives. Even more surprising is that it isn’t able to serve as a container for “AttributeContainer”, which is the parent class for both SoapObject and SoapPrimitive. A SoapObject can only contain other SoapObjects and another type of thing that SoapObject calls a property.
What’s a property? It’s really just another way to represent a SOAP primitive. This may seem like a strange design decision to you. It seems like a strange design decision to me too. But I imagine the library designers had a good reason for doing it this way. In any case, if you want to add a primitive to a SoapObject, you’ll want to use the addProperty method.
findBookRequest.addProperty("ISBN", "0-374-15012-5"); |
findBookRequest.addProperty("ISBN", "0-374-15012-5");
The addProperty method accepts two arguments. The first argument is the name of the parameter or property to add. The second parameter is the actual value of the property.
SoapSerializationEnvelope
Once you have the SoapObject that represents your request, you’ll need to wrap it in a SOAP envelope. ksoap2-android has a couple of classes for working with SOAP envelopes, but the one you’ll most often use is the SoapSerializationEnvelope.
If you browse through the library, you might notice a class called SoapEnvelope and wonder why you wouldn’t use that instead. SoapEnvelope is actually the base class for SoapSerializationEnvelope. Both classes serve as containers for the SOAP message header and body. And both provide mechanisms to serialize/deserialize SOAP data. But the problem with SoapEnvelope is that it doesn’t support SoapObjects or SoapPrimitives. It can only deal with kXML Node types. It’s geared more towards the scenario where you manually build up your own SOAP data structure using kXML and then hand it off to SoapEnvelope for serialization. It’s not very convenient to work with and since you’ll almost always be dealing with SoapObjects or SoapPrimitives, you’ll need to use the SoapSerializationEnvelope.
SoapSerializationEnvelope’s constructor looks like so.
public SoapSerializationEnvelope(int version); |
public SoapSerializationEnvelope(int version);
The constructor accepts one argument – the version of SOAP you intend to use. You might use it like so.
SoapSerializationEnvelope soapEnvelope =
new SoapSerializationEnvelope(SoapEnvelope.VER12); |
SoapSerializationEnvelope soapEnvelope =
new SoapSerializationEnvelope(SoapEnvelope.VER12);
All of the supported versions of SOAP are defined as constants in the SoapEnvelope base class. The versions of SOAP that are currently supported are 1.0, 1.1, and 1.2. The version you choose is dependent on the versions supported by the web service. Consult the web service documentation to see what it supports.
Now you can finally add your request object to the envelope by using the setOutputSoapObject() method.
soapEnvelope.setOutputSoapObject(findBookRequest); |
soapEnvelope.setOutputSoapObject(findBookRequest);
What about SOAP headers?
SOAP headers seem to have been a bit of an afterthought in the design of ksoap2-android. There is no class in the library that represent a SOAP header. The handling of all the header data is managed by the SoapEnvelope base class. And as you might guess from my earlier comments concerning SoapEnvelope, SOAP headers are specified using a kXML data type – Element.
SoapEnvelope has a member variable called headerOut, which is declared as an array of Elements. By default this is null, which results in an empty SOAP header element when serialized. To specify your own set of headers, instantiate a new Element array and populate it appropriately for your application. Here’s an example of how you might do this for some fictional authorization header.
final String SERVICE_NAMESPACE = "ServiceNamespace";
Element authHeader = new Element();
authHeader.setNamespace(SERVICE_NAMESPACE);
authHeader.setName("authHeader");
Element username = authHeader.createElement(SERVICE_NAMESPACE, "username");
username.addChild(Node.TEXT, "skirk");
authHeader.addChild(Node.ELEMENT, username);
Element password = authHeader.createElement(SERVICE_NAMESPACE, "password");
password.addChild(Node.TEXT, "mypassword");
authHeader.addChild(Node.ELEMENT, password);
soapEnvelope.headerOut = new Element[] { authHeader }; |
final String SERVICE_NAMESPACE = "ServiceNamespace";
Element authHeader = new Element();
authHeader.setNamespace(SERVICE_NAMESPACE);
authHeader.setName("authHeader");
Element username = authHeader.createElement(SERVICE_NAMESPACE, "username");
username.addChild(Node.TEXT, "skirk");
authHeader.addChild(Node.ELEMENT, username);
Element password = authHeader.createElement(SERVICE_NAMESPACE, "password");
password.addChild(Node.TEXT, "mypassword");
authHeader.addChild(Node.ELEMENT, password);
soapEnvelope.headerOut = new Element[] { authHeader };
If you know or suspect that your web service was implemented using .NET, set SoapSerializationEnvelope’s dotNet property to true. SoapSerializationEnvelope provides special SOAP encoding support for .NET web services. |
HttpTransportSE
With the SOAP request packaged up in a SoapEnvelope, you’re now ready to send the request to your web service. The SOAP specification allows for any transport mechanism. If you wanted to use SMTP or FTP, for example, you totally could (assuming your web service supported it). But you would have to implement the protocol level support for one of these yourself. Out of the box, ksoap2-android only supports HTTP. And since this is the protocol used by 99% of all web services, you can rest easy knowing that all the hard work has been done for you.
HttpTransportSE is the class you’ll use for communicating with web services over HTTP. In case you’re curious, the “SE” part of the class name means that it’s built around J2SE classes. There’s actually another class, HttpTransport, that’s built around the J2ME connection framework. It’s included as part of the ksoap2-android library, but I’m not entirely sure why you would ever choose this over HttpTransportSE.
To perform a SOAP request, construct an instance of HttpTransportSE and invoke the call method on the instance.
HttpTransportSE htse = new HttpTransportSE(
"http://www.example.com/mywebserviceurl");
htse.call("http://www.example.com/mywebserviceurl/FindBookByISBN",
soapEnvelope); |
HttpTransportSE htse = new HttpTransportSE(
"http://www.example.com/mywebserviceurl");
htse.call("http://www.example.com/mywebserviceurl/FindBookByISBN",
soapEnvelope);
There are four overloads of the HttpTransportSE constructor. The simplest version, as used above, accepts a single argument which is the URL to the web service. A second constructor allows you to specify an additional timeout value. The third and fourth constructors are the same as the first two, except that they allow you to include HTTP proxy information in addition to the other arguments.
The call() method is where all the action happens. It connects to the service, sends the SOAP request, receives the response, parses it, and then returns it back to the caller. It doesn’t get much easier than that. One thing to keep in mind though is that call() is a blocking method. So whichever thread executes this method will block until call() is finished or throws an exception. It probably doesn’t need to be said, but I’ll say it anyway – you’ll want to do this on a thread other than your UI thread or risk a bad user experience and an ANR message.
The call() method accepts two arguments. The first argument is the URL for the SOAP action. This is often appears in HTTP headers as “SOAP-Action”. If your web service documentation includes snippets of sample XML, you’ll most likely see the SOAP-Action header appear in there. The second parameter is the SOAP envelope.
There’s a second flavor of the call() method that accepts a third argument. This extra argument is List which is meant to contain HTTP header information. A typical example where you might use this is when dealing with a web service that performs session management using HTTP cookies (if you do this web service implementer, I hate you). In this scenario, you would need to maintain the cookie data yourself and supply it to the call() method as appropriate. This particular version of the call() method also returns a List containing the HTTP headers supplied by the server. So that’s how you’d most likely get your initial cookie data.
There’s a well-known bug in the way that HTTP connections are reused on some versions of Android. There’s a very good discussion of it in Google Issue #7786 and on StackOverflow. It’s advised that you disable HTTP keep-alive while performing web service communications.
System.setProperty("http.keepAlive", "false"); |
System.setProperty("http.keepAlive", "false");
|
Handling the Server Response
If call() is successful, the SoapSerializationEnvelope that provided the request will now also contain the server response. You can obtain the response by calling the SoapEnvelope’s getResponse() method.
SoapObject response = (SoapObject) envelope.getResponse(); |
SoapObject response = (SoapObject) envelope.getResponse();
The return type of getResponse() is actually declared as Object. But the SOAP data type that’s returned will be one of three things – SoapPrimitive, SoapObject, or a Vector. This is where you really need to understand what sort of data the server is sending back to you. If the server’s response is a simple XML primitive type, the returned object will be a SoapPrimitive. If it’s a complex XML type, the returned object will be a SoapObject. If the server responded with multiple objects, then the returned object will be a Vector of SoapPrimitives and/or SoapObjects. You may need to experiment with this to get it right.
If you’re dealing with a complex data, then the response will be a SoapObject. Just as you are able to set properties in your request, you can also fetch the them from SoapObjects. Continuing our contrived FindBookByISBN example, let’s pretend that the server returned a complex object called FindBookByISBNResult that contains three properties – “title”, “author”, and “publisher”. Using the property names, we can retrieve the values likes so.
String strTitle = null;
String strAuthor = null;
String strPublisher = null;
try
{
if (!soapObject.hasProperty("title"))
strTitle = soapObject.getPropertyAsString("title");
if (!soapObject.hasProperty("author"))
strPublisher = soapObject.getPropertyAsString("author");
if (!soapObject.hasProperty("publisher"))
strPublisher = soapObject.getPropertyAsString("publisher");
}
catch (Exception e)
{
e.printStackTrace();
} |
String strTitle = null;
String strAuthor = null;
String strPublisher = null;
try
{
if (!soapObject.hasProperty("title"))
strTitle = soapObject.getPropertyAsString("title");
if (!soapObject.hasProperty("author"))
strPublisher = soapObject.getPropertyAsString("author");
if (!soapObject.hasProperty("publisher"))
strPublisher = soapObject.getPropertyAsString("publisher");
}
catch (Exception e)
{
e.printStackTrace();
}
Using the hasProperty() method, we first check to see if the property we’re interested in has been set. If the property is there, we fetch it using getPropertyAsString(). SoapObject actually has a number of getter methods for properties. They break down as follows:
// Get property by index.
public Object getProperty(int index);
// Get property by name.
public Object getProperty(String name);
// Get property by index. Returns the toString representation of the property.
public String getPropertyAsString(int index);
// Get property by name. Returns the toString representation of the property.
public String getPropertyAsString(String name);
// The following methods are essentially no-throw versions of the methods described previously.
// Get property by index.
public Object getPropertySafely(final String name);
// Get property by name. Returns default value if property not found.
public Object getPropertySafely(final String name, final Object defaultThing);
// Get property by index. Returns the toString representation of the property.
public String getPropertySafelyAsString(final String name);
// Get property by name. Returns the toString representation of the property or the defaultValue if the property isn't found.
public String getPropertySafelyAsString(final String name, final Object defaultThing) |
// Get property by index.
public Object getProperty(int index);
// Get property by name.
public Object getProperty(String name);
// Get property by index. Returns the toString representation of the property.
public String getPropertyAsString(int index);
// Get property by name. Returns the toString representation of the property.
public String getPropertyAsString(String name);
// The following methods are essentially no-throw versions of the methods described previously.
// Get property by index.
public Object getPropertySafely(final String name);
// Get property by name. Returns default value if property not found.
public Object getPropertySafely(final String name, final Object defaultThing);
// Get property by index. Returns the toString representation of the property.
public String getPropertySafelyAsString(final String name);
// Get property by name. Returns the toString representation of the property or the defaultValue if the property isn't found.
public String getPropertySafelyAsString(final String name, final Object defaultThing)
A Word About SOAP Faults
The discussion wouldn’t be complete without mentioning how ksoap2-android handles SOAP faults. If a web service generates a SOAP fault, the call() method of HttpTransportSE will throw an exception. The exception is, as you might guess, SoapFault. You’ll need to catch these and handle them appropriately. In addition to the Exception message, SoapFault contains a number of useful member variables for things like the fault code, fault actor, etc.
A Real Example
If you made it this far, you’re now ready to put ksoap2-andorid into practice and communicate with a real life SOAP based web-service. The web service I’ve chosen for this example is made available by a company called WebserviceX. WebserviceX makes a number of web services available for use. Some are free and some not so much. The web service we’ll be using is a free currency conversion rate service. It’s very, very basic as you can see from its specification which is found here.
http://www.webservicex.net/CurrencyConvertor.asmx
Source code for the example can be found here.
http://www.shanekirk.com/code/CurrencyRates.zip
The ZIP file contains both the application source code and an Eclipse project. Note that the ksoap2-android library is NOT bundled in the ZIP file. If you wish to build the example project, you’ll need to add the ksoap2-android jar file to your project build path.
The example project should be fairly straightforward. It contains a single Activity that allows the user to determine the conversion rate between two currencies. The user selects the currency to convert from and the currency the convert to using the two Spinner widgets. The user then clicks on the “Get Conversion Rate” button. The click handler for this button spawns off an AsyncTask that communicates with the web service and updates the UI as appropriate.
Go Forth And Code
That brings us to the end of this discussion. There are a few other nifty tricks you can do with ksoap2-android such as mapping SOAP objects to classes and performing secure communications over HTTPS. But I’ll leave more advanced ksoap2-android usage for a future article.
If you found this information useful, by all means leave a comment! I’d love to hear from you. And that goes doubly so for anyone who finds an error. Have fun and build something awesome.