Basic Apache SOAP Coding

While the samples provided with Apache SOAP illustrate a number of the features of the software, beginners often ignore the samples or have a difficult time finding and understanding ones relevant to their needs. Below, I attempt to provide a simple starting point for those new to Apache SOAP. When you have finished, you may want to move on to Advanced Apache SOAP Coding.

The examples and descriptions below assume that Apache SOAP has been installed according to these instructions. To the extent the environment in which you work differs from the one described by those instructions, you will need to mentally adjust the text below.

Primitives and arrays
Beans
Resources

Primitives and arrays

The simplest SOAP coding involves methods for which parameters and return values are primitive Java types or arrays of primitives. (Not to confuse the issue, but some non-primitive Java types, such as Date, Hashtable and Vector, are just as easy to use.) The following is an example of a simple service with two such methods.

/** Simple test of String and String[] parameters and return values */
public class HelloService1 {
    /** Says hello to one person */
    public String helloString(String name) {
        return "Hello, " + name;
    }
    /** Says hello to many people */
    public String[] helloStringArray(String[] names) {
        String[] ret = new String[names.length];
        for (int i = 0; i < names.length; i++)
            ret[i] = "Hello, " + names[i];
        return ret;
    }
}

The most interesting thing to note is that the service in no way references any Apache SOAP code. This is one of the powerful features of Apache SOAP: you can turn any Java code into a service without any changes. The magic is all performed by the deployment descriptor for the service, which is an XML text file. An example is the following.

<!-- Simple test of String and String[] parameters and return values -->
<isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment"
             id="urn:hello1">
  <isd:provider type="java"
                scope="Application"
                methods="helloString helloStringArray">
    <isd:java class="HelloService1"/>
  </isd:provider>
  <isd:faultListener>org.apache.soap.server.DOMFaultListener</isd:faultListener>
</isd:service>

As a beginner, there are three important things to notice in this file. First, the service is given an id, which is a unique URI used to identify the service. In this case, the id is urn:hello1. Second, the Java class implementing the service is specified, HelloService1 in this case. The class name should be fully qualified with any packages. Finally, the methods that are available for the service are listed. This can be used to expose just a subset of the public interface for the class. (Advanced users should note that there is a bug in the EJB providers Apache SOAP 2.2, as they do not enforce this list. Here, we are using the standard Java RPC provider, which does not have this bug.)

There are two things that must be done to deploy the service. First, the byte code file must be available for loading within the context of the Apache SOAP webapp. This means copying the file to the right place and restarting Tomcat. In my environment, this is accomplished by doing the following.

xcopy /y HelloService1.class j:\jakarta-tomcat-4.0.1\webapps\soap\WEB-INF\classes
j:
cd \jakarta-tomcat-4.0.1\bin
shutdown
startup

The second step in deploying the service is to register it with the Apache SOAP router. This is done by running the following.

java org.apache.soap.server.ServiceManagerClient http://localhost:8080/soap/servlet/rpcrouter deploy HelloDD1.xml

The client code to access this service requires more typing, but there is nothing especially difficult about it.

import java.net.*;
import java.util.*;
import org.apache.soap.*;
import org.apache.soap.rpc.*;
import org.apache.soap.encoding.*;
/** Simple test of String and String[] parameters and return values */
public class HelloClient1 {
    /** Gets the type of a class as a string */
    private static String getType(Class c) {
        if (!c.isArray())
            return c.getName();

        return "array of " + getType(c.getComponentType());
    }
    /** Gets the value of an object as a string */
    private static String getValue(Object o) {
        Class c = o.getClass();
        if (!c.isArray())
            return "'" + o.toString() + "'";

        String ret = null;
        Object[] oa = (Object[]) o;
        for (int i = 0; i < oa.length; i++) {
            if (ret == null)
                ret = "[" + getValue(oa[i]);
            else
                ret = ret + ", " + getValue(oa[i]);
        }
        return ret + "]";
    }
    /** Makes a SOAP call */
    private static void makeCall(String targetURI,
                                 String methodName,
                                 String encodingStyleURI, 
                                 SOAPMappingRegistry smr, 
                                 Vector parameters,
                                 String endpointURL) throws Exception {
        Call call = new Call();
        call.setTargetObjectURI(targetURI);
        call.setMethodName(methodName);
        call.setEncodingStyleURI(encodingStyleURI);
        if (smr != null)
            call.setSOAPMappingRegistry(smr);
        call.setParams(parameters);

        System.out.print("Call to method '" +
                         methodName + 
                         "' of object '" +
                         targetURI + "' ");
        Response resp = call.invoke(new URL(endpointURL),
                                    targetURI);

        if (resp.generatedFault()) {
          Fault fault = resp.getFault();
          System.out.println("faulted:");
          System.out.println("  Code = " + fault.getFaultCode());  
          System.out.println("  String = " + fault.getFaultString());
        } else {
          Parameter result = resp.getReturnValue();
          Object value = result.getValue();
          System.out.println("returned:");
          System.out.println("  Type = " + getType(value.getClass()));
          System.out.println("  Value = " + getValue(value));
        }
    }
    /** Tests a service using String and String[] parameters and return values */
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println(
                "Usage: java HelloClient1 SOAP-router-URL name [name ...]");
            System.exit(1);
        }
    
        String targetURI = "urn:hello1";
        String methodName = "helloString";
        String encodingStyleURI = Constants.NS_URI_SOAP_ENC;
        Vector params = new Vector();
        params.addElement(new Parameter("name", String.class, args[1], null));
        String endpointURL = args[0];

        makeCall(targetURI, methodName, encodingStyleURI, null, params, endpointURL);

        methodName = "helloStringArray";
        String[] names = new String[args.length - 1];
        for (int i = 0; i < args.length - 1; i++)
            names[i] = args[i + 1];
        params = new Vector();
        params.addElement(new Parameter("names", names.getClass(), names, null));

        makeCall(targetURI, methodName, encodingStyleURI, null, params, endpointURL);
    }
}

The information required to invoke the service is pretty basic. First, the URI for the service, as specified in the deployment descriptor, is used as the target URI. Second, one of the method names in the deployment descriptor must be specified. Finally, the URL of the SOAP endpoint must be specified. This is the same URL as is used to deploy the service.

The method parameters are specified as a Vector of Parameter instances. For each method parameter, one merely creates a Parameter, initialized with the appropriate values. The most important parts of the parameter are the Java class and the parameter value stored within the Java code. This example shows that the class is completely specified; in particular, if the parameter is an array, the class must be specified as an array. In this case, since the variable names is a String[], using names.getClass() is appropriate.

The client can be run by executing the following.

java HelloClient1 http://localhost:8080/soap/servlet/rpcrouter Tim John Mary Jane

The output looks like:

Call to method 'helloString' of object 'urn:hello1' returned:
  Type = java.lang.String
  Value = 'Hello, Bill'
Call to method 'helloStringArray' of object 'urn:hello1' returned:
  Type = array of java.lang.String
  Value = ['Hello, Bill', 'Hello, Sue', 'Hello, Mary', 'Hello, John']

The TcpTunnelGui utility, included in Apache SOAP, is invaluable is seeing what characters are being sent and received by the client. Start TcpTunnelGui by running this:

java org.apache.soap.util.net.TcpTunnelGui 81 localhost 8080

You capture the SOAP conversation by directing the client to TcpTunnelGui, as in this example.

java HelloClient1 http://localhost:81/soap/servlet/rpcrouter Bill Sue Mary John

The information captured for the first call looks like the following.

POST /soap/servlet/rpcrouter HTTP/1.0
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: 446
SOAPAction: "urn:hello1"

<?xml version='1.0' encoding='UTF-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
                      xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
                      xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<ns1:helloString xmlns:ns1="urn:hello1"
        SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<name xsi:type="xsd:string">Bill</name>
</ns1:helloString>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The response is as follows.

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: 475
Date: Tue, 23 Apr 2002 18:51:37 GMT
Server: Apache Tomcat/4.0.1 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=C75254A2159435D124A04226311196F5;Path=/soap

<?xml version='1.0' encoding='UTF-8'?>
<SOAP-ENV:Envelope
   xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<ns1:helloStringResponse xmlns:ns1="urn:hello1"
   SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return xsi:type="xsd:string">Hello, Bill</return>
</ns1:helloStringResponse>

</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Capturing the exchange for the second SOAP call is left as an exercise for the reader. You can download the source for this example.

Return to top.

Beans

The previous example showed SOAP methods with parameters and return values there were either strings or arrays of strings. While it is possible to build powerful services using just primitive types such as these, one of the powerful capabilities of SOAP is support for user-defined types. For the second example, I will use the following Java class as a parameter and return value.

/** A simple bean for testing */
public class HelloBean {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String toString() {
        return name;
    }
}

This class follows the Java bean pattern: it has a single property called "name" with both accessor (getter) and mutator (setter) methods. As a programmer, it is pretty simple to imagine how an instance of this class might be represented in XML within a SOAP message, such as the following.

<HelloBean>
    <Name>Fred</Name>
</HelloBean>

The question is, how does software such as Apache SOAP decide what XML to write? For that matter, how did it know how to write a string and an array of strings in the first example? And, given a SOAP message containing various data types, how does Apache SOAP decide how to "decode" the XML into Java variables?

In Apache SOAP, the answer is two interfaces, Serializer and Deserializer, and a class, the SOAPMappingRegistry. A serializer is a class that implements Serialzer. It writes an XML representation of some Java variable, whether a primitive type or an instance of some class. A deserializer implements Deserializer and reads XML into a variable of the appropriate type. A mapping registry prescribes the serializer to use for a particular class, and the deserializer to use for a particular XML "type", often specified with the xsi:type attribute.

Apache SOAP has built-in serializers and deserializers for Java primitives (and some additional types, such as Date, Vector and Hashtable). These are referenced by a built-in mapping registry. The first example worked by using this built-in code and data. However, Apache SOAP does not have the built-in ability to handle the HelloBean class. There are two choices of how to handle the HelloBean class. One option is to write a class that implements Serializer and Deserializer for HelloBean. The second option is to use a class provided with Apache SOAP to handle classes that follow the bean pattern. This example uses that class, called BeanSerializer.

As before, the service does not contain any code specific to Apache SOAP.

/** Simple test of HelloBean and HelloBean[] parameters and return values */
public class HelloService2 {
    /** Says hello to one person */
    public HelloBean helloBean(HelloBean name) {
        HelloBean ret = new HelloBean();
        ret.setName("Hello, " + name.getName());
        return ret;
    }
    /** Says hello to many people */
    public HelloBean[] helloBeanArray(HelloBean[] names) {
        HelloBean[] ret = new HelloBean[names.length];
        for (int i = 0; i < names.length; i++) {
            ret[i] = new HelloBean();
            ret[i].setName("Hello, " + names[i].getName());
        }
        return ret;
    }
}

However, there is something new in the deployment descriptor for this example.

<!-- Simple test of HelloBean and HelloBean[] parameters and return values -->
<isd:service xmlns:isd="http://xml.apache.org/xml-soap/deployment"
             id="urn:hello2">
  <isd:provider type="java"
                scope="Application"
                methods="helloBean helloBeanArray">
    <isd:java class="HelloService2"/>
  </isd:provider>
  <isd:faultListener>org.apache.soap.server.DOMFaultListener</isd:faultListener>
  <isd:mappings>
    <isd:map encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
             xmlns:x="urn:hello2ns" qname="x:HelloBean"
             javaType="HelloBean"
             java2XMLClassName="org.apache.soap.encoding.soapenc.BeanSerializer"
             xml2JavaClassName="org.apache.soap.encoding.soapenc.BeanSerializer"/>
  </isd:mappings>    
</isd:service>

This deployment descriptor specifies mappings. Specifically, it specifies that an instance of the Java class HelloBean should be serialized with org.apache.soap.encoding.soapend.BeanSerializer, and that an xsi:type of urn:hello2ns:HelloBean should be deserializer with the same class.

To deploy this service, it is necessary to copy the byte code for both the service and the bean class before registering the service with the SOAP router.

xcopy /y HelloBean.class j:\jakarta-tomcat-4.0.1\webapps\soap\WEB-INF\classes
xcopy /y HelloService2.class j:\jakarta-tomcat-4.0.1\webapps\soap\WEB-INF\classes
j:
cd \jakarta-tomcat-4.0.1\bin
shutdown
startup

Similar to the deployment descriptor, the client has some new code to tell Apache SOAP how to handle the HelloBean.

    /** Tests a service using String and String[] parameters and return values */
    public static void main(String[] args) throws Exception {
        if (args.length < 2) {
          System.err.println(
              "Usage: java HelloClient2 SOAP-router-URL name [name ...]");
          System.exit(1);
        }
    
        SOAPMappingRegistry smr = new SOAPMappingRegistry();
        BeanSerializer beanSer = new BeanSerializer();
        smr.mapTypes(Constants.NS_URI_SOAP_ENC,
                     new QName("urn:hello2ns", "HelloBean"),
                     HelloBean.class, beanSer, beanSer);

        String targetURI = "urn:hello2";
        String methodName = "helloBean";
        String encodingStyleURI = Constants.NS_URI_SOAP_ENC;
        Vector params = new Vector();
        HelloBean helloBean = new HelloBean();
        helloBean.setName(args[1]);
        params.addElement(new Parameter("name", HelloBean.class, helloBean, null));
        String endpointURL = args[0];

        makeCall(targetURI, methodName, encodingStyleURI, smr, params, endpointURL);

        methodName = "helloBeanArray";
        HelloBean[] names = new HelloBean[args.length - 1];
        for (int i = 0; i < args.length - 1; i++) {
            names[i] = new HelloBean();
            names[i].setName(args[i + 1]);
        }
        params = new Vector();
        params.addElement(new Parameter("names", names.getClass(), names, null));

        makeCall(targetURI, methodName, encodingStyleURI, smr, params, endpointURL);
    }

The new code creates a mapping registry, adds a mapping for HelloBean, and uses the registry for the call.

You can download the source for this example as well.

Return to top.

Resources

Using SOAP with Tomcat.
Clean up your wire protocol with SOAP, Part 2.

Return to top.

Copyright © 2002 Scott Nichol.
23-Apr-2002