Level: Advanced Frank Cohen (fcohen@pushtotest.com), CEO, PushToTest
26 May 2003 The IBM Emerging Technologies Toolkit (ETTK) features the Axis toolkit for software developers to enable Java applications as SOAP-based Web services. In this article, Frank Cohen shows Java developers how to use Axis to work with complex datatypes, including JavaBean components.
The ETTK is a kit of tools and libraries for designing, developing, and executing emerging autonomic and Grid-related technologies and Web services. While the ETTK comes with examples that showcase recently announced specifications and prototypes from the IBM emerging technology development and research teams, it provides introductory material to help developers get started with development of Web services. The ETTK evolved from the Web Services Toolkit (WSTK) and includes the Axis library, a full implementation of the SOAP 1.1 specification for Java developers.
Axis makes it easy for Java developers to build SOAP-based Web services from existing Java code. In its easiest form, a Java Archive Resource (JAR) file is dropped into the Axis webapps directory and Axis exposes the public methods as SOAP-based Web services automatically. Consider the following Java class in Listing 1.
Listing 1. Simple Responder Java class
public class Responder
{
public String Repeat( String theword )
{
return "You told me to say: " + theword;
}
}
|
The Responder class has one public method named repeat. It takes a String value and returns a String value. By compiling the Responder class and packaging it into a JAR file you can offer the Responder as a Web service by dropping the JAR file into the Axis webapps directory. Wasn't that very easy? Much more difficult is to expose a Java class with methods that expect to send or receive complex datatypes.
In Listing 2 is the Responder class having a second call that returns a Java Bean.
Listing 2. More complex Responder Java class
public class Responder
{
public String repeat( String theword )
{
return "You told me to say: " + theword;
}
public MyID getMyID()
{
MyID theid = new MyID();
return theid;
}
class MyID
{
private String name = "Frank Cohen";
private int number = 10;
public MyID();
public String getName() { return name; }
public void setName( String aname ) { name = aname; }
public String getNumber() { return number; }
public void setNumber( int num ) { number = num; }
}
}
|
This Responder class includes an inner class that defines a JavaBean component. Unfortunately you can't just drop this compiled JAR into the Axis webapps directory and get access to the getMyID() method. Axis does not understand the complex datatype formatting of the MyID bean and will return a serialization exception.
The SOAP specification defines a set of simple datatypes -- String, Int, Long, and others -- and a means to provide support for complex datatypes. Most SOAP implementations come with an encoder and decoder for complex datatypes, including Java Bean components. This article shows how to use complex datatypes in Apache Axis and Apache SOAP.
As the maintainer of a popular open-source test framework and utility -- TestMaker -- I am in a unique position to see software development practices in action, especially with Web services. Recently, I spent the better part of a day helping a developer at a large company use TestMaker to test a SOAP-based Web service that emits a complex datatype. The developer needed help getting TestMaker to understand the custom response type. She also needed help creating the Web service using Apache Axis on her host.
Here are the tasks she needed to accomplish:
- Create a Web service using Apache Axis on the server that uses the built-in BeanSerializer to handle complex datatypes.
- Create a client to the Web service using Apache SOAP's BeanSerializer class to handle complex datatypes.
- Run a performance test to learn the throughput of the serializers.
This article describes the steps needed to accomplish these tasks. All of the software mentioned in this article is available for free download (see Resources for a link).
Defining the Web service
To begin, you will create a JavaBean representation of the complex datatype. This is shown in Listing 3.
Listing 3. JavaBean representation of complex datatype
package com.pushtotest;
/**
* Forecast Java Bean for the weather Web Service
*/
public class Forecast {
String zip = null;
String city = null;
String state = null;
String date = null;
String forecast = null;
byte hi = 0;
byte low = 0;
byte precip = 0;
/** Creates a new instance of WeatherBean */
public Forecast() {
}
public void setZip( String thezip )
{
zip = thezip;
}
public String getZip()
{
return zip;
}
public void setCity( String thecity )
{
city = thecity;
}
public String getCity()
{
return city;
}
public void setState( String thestate )
{
state = thestate;
}
public String getState()
{
return state;
}
public void setDate( String thedate )
{
date = thedate;
}
public String getDate()
{
return date;
}
public void setForecast( String theforecast )
{
forecast = theforecast;
}
public String getForecast()
{
return forecast;
}
public void setLow( byte thelow )
{
low = thelow;
}
public byte getLow()
{
return low;
}
public void setHi( byte thehi )
{
hi = thehi;
}
public byte getHi()
{
return hi;
}
public void setPrecip( byte theprecip )
{
precip = theprecip;
}
public byte getPrecip()
{
return precip;
}
}
|
Forecast is a straightforward Java class that encapsulates variables for weather reports, including a U.S. zip code, and temperatures. Forecast follows the JavaBean pattern of having a public initializer with no parameters and getter/setter methods for each of the encapsulated variables. As you will see, you will use the Forecast JavaBean component on both the server side and client side.
Next you define a weather Web service. It receives a zip code value and returns a Forecast object. The interface to the weather Web service is described in the the WSDL document shown in Listing 4.
Listing 4. WSDL for weather service
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
targetNamespace="urn:weather"
xmlns:impl="urn:weather"
xmlns:intf="urn:weather"
xmlns:apachesoap="http://xml.apache.org/xml-soap"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns2="http://weather"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://weather">
<import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>
<complexType name="Forecast">
<sequence>
<element name="zip" nillable="true" type="xsd:string"/>
<element name="city" nillable="true" type="xsd:string"/>
<element name="state" nillable="true" type="xsd:string"/>
<element name="date" nillable="true" type="xsd:string"/>
<element name="forecast" nillable="true" type="xsd:string"/>
<element name="hi" type="xsd:byte"/>
<element name="low" type="xsd:byte"/>
<element name="precip" type="xsd:byte"/>
</sequence>
</complexType>
</schema>
</wsdl:types>
<wsdl:message name="getWeatherResponse">
<wsdl:part name="getWeatherReturn" type="tns2:Forecast"/>
</wsdl:message>
<wsdl:message name="getWeatherRequest">
<wsdl:part name="in0" type="xsd:string"/>
</wsdl:message>
<wsdl:portType name="Weather">
<wsdl:operation name="getWeather" parameterOrder="in0">
<wsdl:input name="getWeatherRequest" message="impl:getWeatherRequest"/>
<wsdl:output name="getWeatherResponse" message="impl:getWeatherResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="weatherSoapBinding" type="impl:Weather">
<wsdlsoap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getWeather">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="getWeatherRequest">
<wsdlsoap:body
use="encoded"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
namespace="urn:weather"/>
</wsdl:input>
<wsdl:output name="getWeatherResponse">
<wsdlsoap:body
use="encoded"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
namespace="urn:weather"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="WeatherService">
<wsdl:port name="weather" binding="impl:weatherSoapBinding">
<wsdlsoap:address
location="http://examples.pushtotest.com:92/ axis/servlet/AxisServlet"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
|
To build the server-side of the Web service you define the weather Java class using the code shown in Listing 5.
Listing 5. Weather class
/*
* weather.java
*/
package com.pushtotest;
public class weather {
/** Creates a new instance of weather */
public weather() {
}
public Forecast getWeather( String ziprq )
{
Forecast fb = new Forecast();
fb.setZip( ziprq );
fb.setCity( "Campbell" );
fb.setState( "CA" );
fb.setDate( "April 11, 2003" );
fb.setForecast( "20 percent chance of rain." );
return fb;
}
}
|
The weather class defines a single getWeather method that receives a zip code value in a String object and returns a Forecast object. Since you will be using SOAP RPC encoding, the SOAP stack takes care of unmarshalling the request and then marshalling the Forecast object into a response.
A simple Ant build script compiles the weather and Forecast objects into class files and then into a weather.jar archive file. The Ant build script -- build.xml -- is found in the source files for the Web service (see Resources for a link).
Once built, weather.jar is ready to be installed as a Web service on an Apache Axis server. Apache Axis provides an easy installation mechanism through Web Service Deployment Descriptor (WSDD) formatted files. WSDD is an Axis defined XML format that defines the installation parameters for a Web service. The WSDD for the weather Web service is shown in Listing 6.
Listing 6. WSDD for weather service
<!-- This file deploys the weather Web Service onto an Apache Axis server ->
<deployment
xmlns="http://xml.apache.org/axis/wsdd/"
xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<!-- Services from Weather service -->
<service name="weather" provider="java:RPC" style="rpc" use="encoded">
<parameter name="wsdlTargetNamespace" value="urn:weather"/>
<parameter name="wsdlServiceElement" value="WeatherService"/>
<parameter name="wsdlServicePort" value="weather"/>
<parameter name="className" value="weather.ws.WeatherSoapBindingImpl"/>
<parameter name="wsdlPortType" value="Weather"/>
<operation name="getWeather"
qname="operNS:getWeather"
xmlns:operNS="urn:weather"
returnQName="getWeatherReturn"
returnType="rtns:Forecast"
xmlns:rtns="http://weather" >
<parameter name="in0"
type="tns:string"
xmlns:tns="http://www.w3.org/2001/XMLSchema"/>
</operation>
<parameter name="allowedMethods" value="getWeather"/>
<parameter name="scope" value="Session"/>
<typeMapping
xmlns:ns="http://weather"
qname="ns:Forecast"
type="java:com.pushtotest.Forecast"
serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"
deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
</service>
</deployment>
<service name="forecast" provider="java:RPC">
<parameter name="allowedMethods" value="*"/>
<parameter name="className" value="com.pushtotest.forecast.Forecast"/>
<beanMapping
qname="myNS:Forecast"
xmlns:myNS="urn:ForecastService"
languageSpecificType="java:com.pushtotest.forecast.ForecastBean"/>
</service>
|
There are two ways to install the Web service using this WSDD file. With Axis running, use this at the command line of a shell:
java org.apache.axis.client.AdminClient deploy.wsdd
|
As an alternative, if Axis is not running use this at the command line of a shell:
java org.apache.axis.utils.Admin server deploy.wsdd
|
While trying to install the Web service on Apache Axis 1.1 release candidate 2 (rc2) I found Axis throws exceptions when deploying the weather Web service. Axis reports "class not found" problems with the installation. Checking the Axis users mailing list, I found that there are incompatibilities between Axis 1.1 rc2 and Apache Tomcat 4.0.3. At the time of this writing I have not heard from other Axis users for a workaround to the problem. So I offer my own workaround here. Add the following snippet of configuration codes (shown in Listing 7) to the Axis configuration file at servlet_container/webapps/axis/WEB-INF/server-config.wsdd.
Listing 7.Code snippet
<service name="weather" provider="java:RPC">
<parameter name="allowedMethods" value="*"/>
<parameter name="className" value="com.pushtotest.weather"/>
<beanMapping
qname="myNS:Forecast"
xmlns:myNS="urn:ForecastService"
languageSpecificType="java:com.pushtotest.Forecast"/>
</service>
|
This manually adds the weather Web service to Axis and maps the Forecast object in the response to the Apache Axis BeanSerializer object. The BeanSerializer provides SOAP RPC encoding of any object that follows the JavaBean pattern (described earlier).
Depending on the method you use to install the weather Web service, the Axis service should be restarted to make the Web service available.
PushToTest provides the weather Web service on its free http://examples.pushtotest.com service to help developers, QA technicians and IT managers understand the concepts behind testing Web services for scalability, performance, and functionality. To view the services hosted there try browsing this URL
http://examples.pushtotest.com:92/axis/servlet/AxisServlet. You should see that the weather service offers a single getWeather method.
So far, I have described the implementation details of the weather Web service. You then were able to build the weather Web service and deploy it on an Apache Axis server. Next comes the effort to make the client side.
The client side
Many SOAP libraries are available to build the client-side of the weather Web service. This article presents TestMaker, a test framework and tool that natively supports SOAP-based Web services. Plus, the TestMaker environment is designed to facilitate testing Web services for scalability, performance, and functionality. See the Resources section below for details.
TestMaker is a framework and utility for building intelligent test agents. TestMaker comes with a library of protocol handlers called the Test Object Oriented Library (TOOL.) A TestMaker test agent script is written in Jython (the Python language implemented entirely in Java.) The Jython scripts instantiate TOOL protocol handler objects. When a SOAP protocol handler needs to make a request to a host it uses the included Apache SOAP stack to marshal and unmarshal the request and response.
The WSDL to this service notes that a client may access the service through this URL http://examples.pushtotest.com:92/axis/servlet/AxisServlet. Requests must contain a single String object holding a U.S. zip code. For the purpose of this article, the weather Web service does not really check the weather, but instead returns the zip code in the Forecast object that is returned.
The entire TestMaker test agent script is shown in Listing 8, followed by a step-by-step explanation. The script is named SOAP_BeanSerializer.a and is found in the download archive reference below in the Resources section.
Listing 8. TestMaker script
# Import tells TestMaker where to find Tool objects
from com.pushtotest.tool.protocolhandler import ProtocolHandler,
SOAPProtocol, SOAPBody, SOAPHeader
from com.pushtotest.tool.response import Response
from java.lang import Long, Integer, String
from com.pushtotest import Forecast
# These classes are provided by the Apache SOAP library
from org.apache.soap.encoding.soapenc import BeanSerializer
from org.apache.soap.util.xml import QName
from org.apache.soap import Constants
print "Agent started."
protocol = ProtocolHandler.getProtocol("soap")
body = SOAPBody()
protocol.setBody(body)
protocol.setHost("examples.pushtotest.com")
protocol.setPath("axis/servlet/AxisServlet")
protocol.setPort( 92 )
# Send a request to the Forecast.getForecast() method on the host
body.setTarget("weather")
body.setMethod("getWeather")
# The service accepts a zip code parameter. The service will response with
# a Java Bean containing the forecast (which is made-up for this example.)
body.addParameter( "ziprq", String, "95008", None )
# Tells the Apache SOAP library to use the BeanSerializer when it receives
# a response from the ForecastService.
beanSer = BeanSerializer()
qName = QName("urn:ForecastService", "Forecast")
protocol.setMapTypes( Constants.NS_URI_SOAP_ENC, qName, Forecast, beanSer, beanSer )
response = protocol.connect()
print "Response from host:"
print response
print "Ended."
|
The TestMaker script begins by using the import command to identity objects in the TOOL library and Apache SOAP library that will be used later in the script:
# Import tells TestMaker where to find Tool objects
from com.pushtotest.tool.protocolhandler import ProtocolHandler,
SOAPProtocol, SOAPBody, SOAPHeader
from com.pushtotest.tool.response import Response
from java.lang import Long, Integer, String
from com.pushtotest import Forecast
# These classes are provided by the Apache SOAP library
from org.apache.soap.encoding.soapenc import BeanSerializer
from org.apache.soap.util.xml import QName
from org.apache.soap import Constants
|
With the objects imported, the script instantiates protocol handler objects:
protocol = ProtocolHandler.getProtocol("soap")
|
The script instantiates a new SOAP protocol handler. This handler is referred to by the protocol variable:
body = SOAPBody()
protocol.setBody(body)
|
The script instantiates a new SOAPBody object to hold the zip code parameter that will be sent in the request to the weather Web service.
The script identifies where to find the weather Web service by identifying the host by using a URL, path, and port number:
protocol.setHost("examples.pushtotest.com")
protocol.setPath("axis/servlet/AxisServlet")
protocol.setPort( 92 )
|
The script identifies the getWeather method of the weather object on the host as the destination target for the request to the Web service:
body.setTarget("weather")
body.setMethod("getWeather")
|
The service accepts a zip code parameter encoded in a simple String object:
body.addParameter( "ziprq", String, "95008", None )
|
The addparameter method provides a lot of flexibility. For example, if the weather Web service received a Forecast object, instead of responding with one, you could use this command:
body.addParameter( "theForecast", Forecast, myForecast, None )
|
This would tell TOOL to encode the myForecast object, which is a Forecast object type, and send it to the host. In other words, any object (Jython or Java) may be marshalled through TOOL's addParameter method.
If you instructed the script to make the request to the weather Web service without any extra configuration the TestMaker SOAP stack on the client side will throw an exception like that shown in Listing 9.
Listing 9. Exception
com.pushtotest.tool.ToolException: Error making SOAP RPC call: No Deserializer found to
deserialize a 'urn:ForecastService:Forecast'
using encoding style 'http://schemas.xmlsoap.org/soap/encoding/'.
at com.pushtotest.tool.protocolhandler.SOAPProtocol.soapRpcCall(SOAPProtocol.java:297)
at com.pushtotest.tool.protocolhandler.SOAPProtocol.connect(SOAPProtocol.java:254)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:324)
at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java)
at org.python.core.PyMethod.__call__(PyMethod.java)
at org.python.core.PyObject.__call__(PyObject.java)
at org.python.core.PyInstance.invoke(PyInstance.java)
at org.python.pycode._pyx5.f$0(D:\agents\Serializers\SOAP_BeanSerializer.a:52)
at org.python.pycode._pyx5.call_function(D:\agents\Serializers\SOAP_BeanSerializer.a)
at org.python.core.PyTableCode.call(PyTableCode.java)
at org.python.core.PyCode.call(PyCode.java)
at org.python.core.Py.runCode(Py.java)
at org.python.core.__builtin__.execfile_flags(__builtin__.java)
at org.python.util.PythonInterpreter.execfile(PythonInterpreter.java)
at com.pushtotest.testmaker_module.agentRunner$1$Job.run(Unknown Source)
at org.netbeans.core.execution.RunClassThread.run(RunClassThread.java:119)
com.pushtotest.tool.ToolException: com.pushtotest.tool.ToolException: Error making
SOAP RPC call: No Deserializer found to
deserialize a 'urn:ForecastService:Forecast'
using encoding style 'http://schemas.xmlsoap.org/soap/encoding/'.
|
The TestMaker SOAP stack on the client side does not know what to do with the Forecast object in the response from the weather Web service. A serializer object maps an XML response into a Java object.
Fortunately the Apache SOAP library includes a BeanSerializer class just like Apache Axis on the server side did above. By adding a few lines of TestMaker script the Apache SOAP library in TestMaker will use the BeanSerializer class to unmarshal the Forecast object in the response.
Earlier, this agent script imported the Forecast object using:
from com.pushtotest import Forecast
|
This tells the class loader to look through the TestMaker classpath for the com.pushtotest.Forecast object. TestMaker maintains its own classpath to facilitate running the same test agents remotely in a companion product from PushToTest called TestNetwork. Details on TestNetwork are found below in the Resources section.
The Forecast object is compiled into the weather.jar script that was earlier loaded onto the Apache Axis host. Now TestMaker references the same weather.jar object on the client-side. To do so edit the testmaker_home/testmaker/bin/runide.cfg file and add:
-cp:a D:/agents/Serializers/dist/weather.jar
|
You will need to modify the path to match the configuration on your machine running TestMaker.
The next step in the SOAP_BeanSerializer.a script is to tell TestMaker how to handle the Forecast Java Bean in the response from the weather Web service:
beanSer = BeanSerializer()
qName = QName("urn:ForecastService", "Forecast")
protocol.setMapTypes( Constants.NS_URI_SOAP_ENC, qName, Forecast, beanSer, beanSer )
|
This tells the Apache SOAP library to use the BeanSerializer when it receives a Forecast complex object type response from the weather Web service.
The connect method instructs the TOOL protocol handler to marshal the request and contact the weather Web service host:
response = protocol.connect()
|
Upon completion the connect method returns a new response object:
print "Response from host:"
print response
|
See the documentation on TOOL for a complete list of methods provided in the response object. For the sake of brevity, this script simply prints the response object to display the contents of the Forecast JavaBean component.
A successful call to the weather Web service host will look like Listing 10 in TestMaker's output window.
Listing 10. Successful call to weather service host
Agent running: SOAP_BeanSerializer.a
Description:
SOAP-based Web Services may send and receive complex datatypes. This agent
accesses a Web Service that returns a Java Bean containing several values.
The script uses the Apache SOAP BeanSerializer to unmarshal the Java Bean
encoded response from the examples.pushtotest.com host.
Agent started.
Response from host:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Body>
<ns1:getWeatherResponse
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:ns1="weather">
<getWeatherReturn href="#id0"/>
</ns1:getWeatherResponse>
<multiRef id="id0" SOAP-ENC:root="0"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xsi:type="ns3:Forecast" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/:encodingStyle"
xmlns:ns3="urn:ForecastService">
<date xsi:type="xsd:string">April 11, 2003</date>
<zip xsi:type="xsd:string">95008</zip>
<city xsi:type="xsd:string">Campbell</city>
<state xsi:type="xsd:string">CA</state>
<forecast xsi:type="xsd:string">20 percent chance of rain.</forecast>
<low xsi:type="xsd:byte">0</low>
<hi xsi:type="xsd:byte">0</hi>
<precip xsi:type="xsd:byte">0</precip>
</multiRef>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Ended.
|
 |
Conclusion
This article presented the technique to use complex datatypes in a Web service. On the server side, you used Apache Axis to create a Web service that receives a simple data type and responds with a JavaBean component encoded in a complex datatype. On the client side, you used TestMaker -- that in-turn used Apache SOAP -- to send the request and receive the JavaBean component.
Download | Name | Size | Download method |
|---|
| SerializerKit.zip | | HTTP |
Resources
- Download the source code used in this article.
- These books and articles may help you understand SOAP serializers:
About the author  | |  | Frank Cohen is the "go to" guy when enterprises need to test and solve problems in complex interoperating information systems, especially Web services. Frank is Founder of PushToTest, a test automation solutions business and author of several books on testing information systems. For the past 20 years he led some of the software industry's most successful products, including Norton Utilities for the Macintosh, Stacker, and SoftWindows. He began by writing operating systems for microcomputers, helping establish video games as an industry, helping establish the Norton Utilities franchise, leading Apple's efforts into middleware and Internet technologies, and most recently serving as principal architect for the Sun Community Server, Inclusion.net (OTC: IINC), and TuneUp.com. He serves as an active member and past board member of the Software Developers Forum, the leading computer software industry association in the Silicon Valley of California. You can reach Frank at fcohen at pushtotest.com. |
Rate this page
|