O'Reilly Book Excerpts: Java and SOAP

Editor's Note: This is the first in a series of excerpts from "Chapter 5: Working with Complex Data Types" of Java and SOAP. This excerpt covers passing arrays as parameters.

In the previous chapter, we created RPC-style services in Java. Those services dealt only with simple data types. Simple data types may suffice in many situations, but you'll eventually need to use more complex data types, like those you're familiar with in your day-to-day Java programming. In this chapter we'll look at creating services with method parameters and return values that are arrays and Java beans. We'll hold off on custom data types until the next chapter, since it's possible that any custom types you create would use the complex data types we'll be discussing here.

Passing Arrays as Parameters

Let's face it--arrays are probably the most common complex data type in programming. They're everywhere, so their use in SOAP is critical. We covered the details of SOAP arrays back in Chapter 3, so you should be aware of how arrays are encoded. So let's get right into writing some Java code for services that use arrays.

In this Series

We've been working with stock market examples, so let's stick with that theme. It might be useful to have a service that returns information about a collection of stock symbols. It might provide the total volume of shares traded for the day, the average trading price of those stocks, the number of stocks trading higher for the day, etc. There are lots of possibilities. Let's start out with a service that returns the total number of shares traded for the day. The service is called urn:BasicTradingService, and it has a method called getTotalVolume. Here is the Java class that implements the service:

package javasoap.book.ch5;
public class BasicTradingService {
   
   public BasicTradingService(  ) {}
  
   public int getTotalVolume(String[] stocks) {
      
      // get the volumes for each stock from some
      // data feed and return the total
      int total = 345000; 
      return total;
   }
}

The BasicTradingService class contains the method getTotalVolume( ), which returns the total number of shares traded. Since we're not going to access a data feed, we'll just return a made-up value. The method returns an integer and takes a single parameter called stocks that is an array of String values. The strings in the array are the stock symbols; in a real application, we'd retrieve the volume for each stock from our data feed and return the total for all the stocks in the array.

Apache SOAP has built-in support for arrays, so you don't need to do anything special on the service side. This also means that there's nothing new in the deployment descriptor; it's built just like it was in the previous chapter. Here is the deployment descriptor for the urn:BasicTradingService service:

<isd:service
       xmlns:isd="http://xml.apache.org/xml-soap/deployment"
       id="urn:BasicTradingService">
  <isd:provider type="java"
       scope="Application"
       methods="getTotalVolume">
    <isd:java class="javasoap.book.ch5.BasicTradingService"  
       static="false"/>
  </isd:provider>
  <isd:faultListener>org.apache.soap.server.DOMFaultListener
  </isd:faultListener>
  <isd:mappings>
  </isd:mappings>    
</isd:service>

You can deploy the service using this deployment descriptor, or you can use the Apache SOAP Admin tool from your browser.

Now let's write a client application that accesses the service. This application is similar to some of the examples from the previous chapter; it differs only in that the parameter we're passing is an array of String instances, rather than a single object. Here is the code:

package javasoap.book.ch5;
import java.net.*;
import java.util.*;
import org.apache.soap.*;
import org.apache.soap.rpc.*;
public class VolumeClient {
   public static void main(String[] args) throws Exception {
      
      URL url = new URL("http://georgetown:8080/soap/servlet/rpcrouter");
    
      Call call = new Call(  );
      call.setTargetObjectURI("urn:BasicTradingService");
      
      call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC);
      String[] stocks = { "MINDSTRM", "MSFT", "SUN" };
      Vector params = new Vector(  );
      params.addElement(new Parameter("stocks", 
                            String[].class, stocks, null));
      call.setParams(params);
      
      try {
         call.setMethodName("getTotalVolume");
         Response resp = call.invoke(url, "");
         Parameter ret = resp.getReturnValue(  );
         Object value = ret.getValue(  );
         System.out.println("Total Volume is " + value);
      }
      catch (SOAPException e) {
         System.err.println("Caught SOAPException (" +
                         e.getFaultCode(  ) + "): " +
                         e.getMessage(  ));
      }
   }
}

We passed String[].class as the second parameter of the Parameter constructor. That identifies the stocks variable as an array of strings. That's it. Nothing else is required. If you run this example, the output should be:

Total Volume is 345000

Let's take a look at the SOAP envelope that was passed to the server for the invocation of the getTotalVolume service method:

<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:getTotalVolume xmlns:ns1="urn:BasicTradingService" 
      SOAP-ENV:encodingStyle=
           "http://schemas.xmlsoap.org/soap/encoding/">
       <stocks  
         xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/"  
         xsi:type="ns2:Array" 
         ns2:arrayType="xsd:string[3]">
            <item xsi:type="xsd:string">MINDSTRM</item>
            <item xsi:type="xsd:string">MSFT</item>
            <item xsi:type="xsd:string">SUN</item>
       </stocks>
    </ns1:getTotalVolume>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The stocks element represents the string array parameter that we passed to the service method. It is typed as an array by setting the xsi:type attribute to ns2:Array. The ns2 namespace identifier was defined on the previous line. Next, the ns2:arrayType attribute is assigned a value of xsd:string[3]. This means that this is an array of size 3, and that each element of the array is an xsd:string. There are three child elements of stocks, each one named item. Remember that the name used for the array elements doesn't matter, and that different SOAP implementations use different schemes for naming these elements. Each element is explicitly typed by setting the xsi:type attribute to the value xsd:string.

This example uses a homogeneous array, i.e., all of the elements of the array are instances of the same type. You may have occasion to use heterogeneous arrays as well, so let's look at that possibility. In Java, arrays are often used as parameters to methods that, in other languages, would have a variable-length parameter list. For instance, the printf( ) function in the C language doesn't have a fixed number of parameters. Even though Java doesn't support this capability, you can simulate it by passing your parameter values in an array. An array can be of any size, and the array elements aren't required to have the same type.

There are certainly other ways to design the interface to a method like this, and this is not the design I'd choose. This situation probably calls for using a custom class or Java bean. However, the approach I've used in this example demonstrates the use of heterogeneous arrays.

Let's add a method to the urn:BasicTradingService that takes a single heterogeneous array as a parameter. The method is called executeTrade. Its parameter is an array containing the stock symbol, the number of shares to trade, and a flag indicating whether it's a buy or sell order (true means buy). The return value is a string that describes the trade (see sidebar). Here is the modified BasicTradingService class:

package javasoap.book.ch5;
public class BasicTradingService {
   
   public BasicTradingService(  ) {
   }
   public int getTotalVolume(String[] stocks) {
      
      // get the volumes for each stock from some
      // data feed and return the total
      
      int total = 345000; 
      return total;
   }
   public String executeTrade(Object[] params) {
      String result;
      try {
         String stock = (String)params[0];
         Integer numShares = (Integer)params[1];
         Boolean buy = (Boolean)params[2];
         String orderType = "Buy";
         if (false == buy.booleanValue(  )) {
            orderType = "Sell";
         }
         result = (orderType + " " + numShares + " of " + stock);
      }
      catch (ClassCastException e) {
         result = "Bad Parameter Type Encountered";
      }
      return result;
   }
}

There is only one parameter for the executeTrade( ) method, an Object[] called params. The objects in this array must be cast to their corresponding types: a String, an Integer, and a Boolean. I like to put class casts inside a try/catch block in case the caller makes a mistake. That way I can do something useful if the method is called incorrectly, even if that means simply returning a description of the error. In Chapter 7, we'll look at generating SOAP faults for situations like this. The information passed in the array is used to generate a string that describes the parameters, and that string is stored in the result variable that is returned to the caller.

Now we can modify the client application so that it passes an appropriate Object[]as the parameter to the executeTrade service method.

The multiParams variable is declared as an Object[], and is populated with the String MINDSTRM, an Integer with the value of 100, and a Boolean with the value of true. Since we're using an array of Java Object instances, we don't use Java primitives as elements of the array. Instead we wrap those primitive values in their Java object equivalents. The second parameter of the Parameter constructor is Object[].class, which is the class for an array of object instances.

package javasoap.book.ch5;
import java.net.*;
import java.util.*;
import org.apache.soap.*;
import org.apache.soap.rpc.*;
public class TradingClient {
   public static void main(String[] args) 
      throws Exception {
      
      URL url = 
        new URL(
          "http://georgetown:8080/soap/servlet/rpcrouter");
    
      Call call = new Call(  );
      call.setTargetObjectURI("urn:BasicTradingService");
      
      call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC);
      Object[] multiParams = { "MINDSTRM", new Integer(100), 
                                    new Boolean(true) };
      Vector params = new Vector(  );
      params.addElement(new Parameter("params", 
                           Object[].class, multiParams, null));
      call.setParams(params);
      
      try {
         call.setMethodName("executeTrade");
         Response resp = call.invoke(url, "");
         Parameter ret = resp.getReturnValue(  );
         Object value = ret.getValue(  );
         System.out.println("Trade Description: " + value);
      }
      catch (SOAPException e) {
         System.err.println("Caught SOAPException (" +
                         e.getFaultCode(  ) + "): " +
                         e.getMessage(  ));
      }
   }
}

If all goes well, the result of executing the executeTrade( ) service method is:

Trade Description: Buy 100 of MINDSTRM

We could force the service object down another path by changing the order of the parameters in the multiParams array. In this case, we would encounter a class cast exception, and the method would return an error string.

Here is the SOAP envelope for the proper invocation of the executeTrade( ) service method:

<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:executeTrade xmlns:ns1="urn:BasicTradingService" 
      SOAP-ENV:encodingStyle=
          "http://schemas.xmlsoap.org/soap/encoding/">
  
      <params  
          xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" 
          xsi:type="ns2:Array" ns2:arrayType="xsd:anyType[3]">
        <item xsi:type="xsd:string">MINDSTRM</item>
        <item xsi:type="xsd:int">100</item>
        <item xsi:type="xsd:boolean">true</item>
      </params>
    </ns1:executeTrade>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

In Chapter 3, we talked about using ur-type to represent any possible data type. Here we see the use of anyType for that purpose. The XML Schema Part 0 recommendation, dated May 2, 2001, explains this in section 2.5.4 as follows: "The anyType represents an abstraction called the ur-type which is the base type from which all simple and complex types are derived. An anyType type does not constrain its content in any way."

The params element is typed by assigning the xsi:type attribute the value of ns2:Array. The only difference from the homogeneous case is that every array element has a different value assigned to the ns2:arrayType attribute. The value xsd:anyType[3] indicates that the array contains 3 elements, each of which can be of any valid data type (see sidebar).

Now let's take a look at passing arrays as parameters using GLUE. The BasicTradingService class can be deployed in GLUE without modification. We'll use a simple Java application to get this service started:

package javasoap.book.ch5;
import electric.util.Context;
import electric.registry.Registry;
import electric.server.http.HTTP;
public class BasicTradingApp {
   public static void main( String[] args )
      throws Exception {
    
      HTTP.startup("http://georgetown:8004/glue");
      Context context = new Context(  );
      context.addProperty("activation", "application");
      context.addProperty("namespace", 
                               "urn:BasicTradingService");
      Registry.publish("urn:BasicTradingService",
         javasoap.book.ch5.BasicTradingService.class, context );
   }
}

Compile and execute the application, and the service is deployed. Now let's write a simple example to access the service using the GLUE API. First let's look at the interface to the service, IBasicTradingService:

package javasoap.book.ch5;
public interface IBasicTradingService {
  int getTotalVolume(String[] symbols);
  String executeTrade(Object[] params);
}

Now we can write an application that binds to the service and calls both its methods:

package javasoap.book.ch5;
import electric.registry.RegistryException;
import electric.registry.Registry;
public class BasicTradingClient {
   public static void main(String[] args) throws Exception 
   {
      try {
        IBasicTradingService srv = 
          (IBasicTradingService)Registry.bind(
            "http://georgetown:8004/glue/urn:BasicTradingService.wsdl",
            IBasicTradingService.class);
        String[] stocks = { "MINDSTRM", "MSFT", "SUN" };
        int total = srv.getTotalVolume(stocks);
        System.out.println("Total Volume is " + total);
        Object[] multiParams = { "MINDSTRM", new Integer(100), 
                                    new Boolean(true) };
        String desc = srv.executeTrade(multiParams);
        System.out.println("Trade Description: " + desc);
    }
    catch (RegistryException e)
    {
       System.out.println(e);
    }
  }
}

As we've seen before, GLUE allows us to use familiar Java programming syntax without having to think about the underlying SOAP constructs. This holds true for the passing of array parameters as well. Everything is pretty much handled for us after the interface is bound to the service.

There are some interesting things to see in the SOAP request envelopes generated by this example. Here is the SOAP envelope for the getTotalVolume( ) method invocation:

<soap:Envelope 
  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' 
  xmlns:xsd='http://www.w3.org/2001/XMLSchema' 
  xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' 
  xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/' 
  soap:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>
    <soap:Body>
      <n:getTotalVolume xmlns:n='urn:BasicTradingService'>
        <arg0 href='#id0'/>
      </n:getTotalVolume>
      <id0 id='id0' soapenc:root='0' 
        xmlns:ns2='http://www.themindelectric.com/package/java.lang/'           
        xsi:type='soapenc:Array' soapenc:arrayType='xsd:string[3]'>
         <i xsi:type='xsd:string'>MINDSTRM</i>
         <i xsi:type='xsd:string'>MSFT</i>
         <i xsi:type='xsd:string'>SUN</i>
      </id0>
    </soap:Body>
</soap:Envelope>

The Body element starts off like we've seen before; the envelope is qualified by a namespace identifier, soap, which represents the namespace of the SOAP envelope. The first child element of the SOAP body is getTotalVolume, which of course is the name of the service method being invoked. getTotalVolume is namespace-qualified using the name of the service. The only child element of getTotalVolume is arg0, which represents the parameter passed to the method. But this isn't the array we passed; it's a reference to the array. This is a significant difference between the ways that GLUE and the Apache SOAP API generate this call. Apache SOAP puts the array in the envelope as a child element of getTotalVolume, and GLUE uses a reference and serializes the array after the getTotalVolume element terminates. So the parameter is serialized as arg0, and includes an href attribute with the value #id0. No data is included, as the array resides elsewhere.

The array that we passed as a parameter follows the getTotalVolume element. It's named id0, although the element name itself, which is generated by GLUE, is not important. The id attribute is assigned a value of id0, which coincides with the href value used in the getTotalVolume element. GLUE generates the soapenc:root attribute with a value of 0, meaning that this element is not considered the root of an object graph. (GLUE seems to include that attribute automatically.) Next we see a declaration of a namespace identifier called ns2 that seems to identify the internal GLUE package for java.lang; however, the ns2 namespace identifier is never used. The xsi:type and soapenc:arrayType attributes are set up in the same way as in the Apache SOAP examples. Finally, the elements of the array are serialized. The only difference between this example and the one generated by Apache SOAP is in the name of the array elements themselves. Apache SOAP named these elements item, and GLUE named them i. The names don't matter; the result is the same.

This example gives us a good opportunity to see two equally valid ways to serialize arrays. It's important that SOAP implementations understand these different styles if they are to interoperate. This is one of the reasons we keep showing the SOAP envelopes generated by the examples. Becoming familiar with the various styles of serialization will help you down the road if you run into problems getting applications based on different implementations to communicate correctly. (Did I say if?)

Let's take a look at the SOAP envelope for the call to the executeTrade service method. This method takes a heterogeneous array as a parameter. It too uses a reference to a separately serialized array, this time encoded as xsd:anyType:

<soap:Envelope 
  xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' 
  xmlns:xsd='http://www.w3.org/2001/XMLSchema' 
  xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' 
  xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/' 
  soap:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>
    <soap:Body>
      <n:executeTrade xmlns:n='urn:BasicTradingService'>
        <arg0 href='#id0'/>
      </n:executeTrade>
      <id0 id='id0' soapenc:root='0' 
        xmlns:ns2='http://www.themindelectric.com/package/java.lang/' 
        xsi:type='soapenc:Array' soapenc:arrayType='xsd:anyType[3]'>
        <i xsi:type='xsd:string'>MINDSTRM</i>
        <i xsi:type='xsd:int'>100</i>
        <i xsi:type='xsd:boolean'>true</i>
      </id0>
    </soap:Body>
</soap:Envelope>

In the next installment, learn about Returning Arrays.


View catalog information for Java and SOAP

Return to ONJava.com.