If your company is like most, it is likely that your suppliers,
vendors, distributors, and partners have exposed dozens of Web
services for your use.
What do you do if you want to try several Web services before
deciding whether to integrate them with your IT environment, and you
don't have the time to write a client for each? And what is the best
way to handle verbose results that make it difficult to extract the
information that is meaningful to your company?
Although some companies let you download free tools to invoke Web
services, they generally don't give you much understanding of how a
request is generated and usually won't go beyond showing you a raw
response. You may want to roll up your sleeves and explore the many
standards and APIs such as SAAJ and XSLT that are available to see
how you can invoke Web services and generate useful reports. During
this process you can begin building a framework for integrating Web
services into your environment.
In this article we'll describe how to develop a generic tool that
allows you to select the WSDL file of any Web service, provide the
required input when prompted, run the Web service to view the result,
and construct a report containing only the data of interest to you.
Our generic tool, which we will call WSClient for the purposes of
this discussion, uses industry standards and readily available Java
APIs such as WSDL4J, SAAJ, Castor, JDOM, and XSLT.
WSClient Overview
Our WSClient will allow us to select a WSDL file, regardless of
whether it resides on our local machine, a private intranet, or the
Internet. After loading and analyzing the file, WSClient will display
the available operations - such as GetFundQuote, GetHistoricalQuote,
and so on. For each defined operation, WSClient will display the
structure of the corresponding input and output messages.
When we select an operation, our tool will create a dummy XML message
that conforms to the operation schema. We can then fill in the input
values and click the Execute button to run the Web service. WSClient
will then send a SOAP message to invoke the Web service and display
the response, which will be a valid result or a SOAP fault describing
a failure.
Let's look at how we'll load the WSDL file, create sample XML input,
invoke the Web service through SOAP, and generate a report from the
response.
WSDL Parsing and Analysis
WSDL (Web Services Description Language) defines an XML grammar that
is used to describe Web services. WSDL separates the abstract
definitions of messages and endpoints from their concrete data
formats and deployments.
WSDL describes Web services by providing the details of the
communication requirements necessary for a client to invoke the Web
service. Specifically, WSDL is an XML model that describes the
messages that need to be exchanged between a client and a service
provider. Additionally, WSDL describes how and where a Web service is
invoked.
A WSDL document uses the elements shown in Table 1 for defining a Web service.
Each element will be used to supply the information we need to invoke
a Web service. WSClient will use IBM's Web Services Description
Language for Java Toolkit (WSDL4J) to programmatically analyze the
structure of the WSDL and identify the operations available for
consumption. WSDL4J is the open source reference implementation of
the Java APIs for WSDL (JWSDL) being developed under the Java Community Process.
Our demonstration Web service is named "XigniteQuotes" and is
available to the public from Xignite, Inc. The XigniteQuotes Web
service returns a full month of historical quotes for any U.S.
Equity. (You can view the WSDL for this Web service at
www.xignite.com/xquotes.asmx?WSDL.) WSClient will be able to read
this service description to get the details required to consume this Web service.
Finding the Services and Operations
Our tool defines a class named ComponentBuilder that uses WSDL4J, the
Castor Schema Object Model, and JDOM to analyze and compose an
in-memory model of the Web service. The ComponentBuilder class
creates an instance of WSDL4J's WSDLReader interface to load the WSDL
definition. The WSDLReader interface defines the necessary methods
for us to turn any WSDL into an in-memory model of a service
description. We pass to the readWSDL() method the URI of our WSDL
document and receive an instance of the Definition interface.
The Definition interface defines the methods we need to begin
analyzing the WSDL definition and makes it possible to
programmatically discover the defined services, their operations, the
data types, and the service's endpoint URI.
Now that we have an in-memory definition of the WSDL document, we ask
for the services that are defined. The Definition interface's
getServices() method returns a collection of Service instances that
our tool will iterate. Each Service instance will contain a group of
related ports, and each port is represented as an instance of the
Port interface. From the Port instance we can discover the binding
referred to by the defined port. A binding is represented as an
instance of the Binding interface, which gives us information on the
binding operations that are represented as instances of the
BindingOperation interface (see Listing 1).
Finding the Parts
Using the BindingOperation we can find details about the concrete
implementation of an abstract operation defined in WSDL. Our tool
currently supports only SOAP operations. WSDL4J's support for
extensions to WSDL include the ExtensibilityElement interface. WSDL4J
defines the SOAPOperation as a type of ExtensibilityElement to give
us the information we need about the concrete implementation. From
the SOAPOperation instance we can determine the encoding style and
the SOAP action URI if one is defined.
The defined operation is represented as an instance of the Operation
interface that is obtained by calling the BindingOperation's
getOperation() method. The message structures of the operation's
input and output are accessible as Message objects. Message
definitions are obtained by calling the getMessage() method defined
on the Input and Output interfaces.
The Message interface defines a method named getParts() to retrieve
the SOAP parts that have been defined for this message. A message
part is represented as an instance of the Part interface. The Part
interface defines methods for retrieving a part's name, element name,
and data type name (see Listing 2).
We can use this information to get the associated schema for each
message part. This allows us to construct the request message to be
sent to the Web service and to process the response generated by the
invocation.
Creating Sample XML Input
Now that we know our message parts, we can create sample XML to use
as request data when invoking the Web service. The Definition
interface defines a method called getTypes() that we call to get the
instance of the Types interface. The WSDL4J Types interface
represents the <types> element defined in a WSDL document. We read
the schema defined by the types element into an in-memory model using
the Castor API.
In our sample WSDL document our <types> element contains the schema
shown in Listing 3.
For each complex message part, we will refer to this schema to build
an XML message that will be passed as part of the SOAP message sent
during the Web service invocation. If we expect that the response
part will be of a complex type we can refer to the type defined in
the schema to process the response message.
The WSClient's ComponentBuilder class defines a method named
buildMessageText() that takes as its input a WSDL4J Message instance. From the Message object we can obtain the list of parts that define
the message. The parts are iterated and sample input text is built
for the message parts. For each part processed we check to see if
there is a complex type defined for it in our Castor Schema Object
Model.
For each message part, we generate an initial XML instance that can
be used to invoke the Web service. For example, our demonstration Web
service takes as the request message the element named
"GetQuotesHistorical". Using the element defined in the schema above
an appropriate input message would be:
<GetQuotesHistorical xmlns="http://www.xignite.com/services/">
<Symbol>SUNW</Symbol>
<Month>12</Month>
<Year>2002</Year>
</GetQuotesHistorical>
This generated message is then saved to our OperationInfo instance
and is then used as the initial message to be sent when we invoke the
service (see Listing 4).
SOAP Invocation
WSClient uses the Java SAAJ API to invoke the Web service. SAAJ
provides a flexible, yet fairly straightforward, way to consume a Web
service. During our WSDL analysis we captured the elements necessary
to make a Web service invocation using SAAJ.
The sequence of consuming a Web service from our SAAJ client will be:
- Create a connection
- Create the message
- Add message content
- Send the message to the destination
- Process the response
Creating a Connection and Message
We create the connection by calling the SOAPConnectionFactory's
newInstance() method. We then create a new SOAPMessage instance using
SAAJ's MessageFactory class. The SOAPMessage instance created by the
SAAJ factory comes initialized with a pre-defined SOAPPart that
contains a SOAPEnvelope (see Listing 5).
In addition, the SOAPEnvelope comes prebuilt with an empty SOAPHeader
and SOAPBody. The SOAPHeader is optional. Our example doesn't pass a
SOAPHeader, so we will remove it from the envelope when building the
request.
Adding Content
We add content to the SOAPBody using SOAPElement instances added as
child elements. We add to the SOAPBody a SOAPElement that contains
the name of the service we are calling. Parts defined for the input
message are then added as child SOAPElements (see Listing 6).
Invoking the Web Service
The request content is obtained from the sample input message that
was built during WSDL analysis. This initial value could be displayed
and edited by the user using a GUI-based tool and then used to
populate the request message.
Once we've added the request content to the SOAPBody we can invoke
the service. The final steps are to create the SOAPAction header and
a URLEndpoint that points to the service's target URL. We give the
Connection instance our populated SOAPMessage and the URLEndpoint and
invoke the Connection's call() method. The call() method returns the
XML response as a SOAPMessage (see Listing 7).
Making Sense Out of the Response
Web service responses can be very verbose and may contain extraneous
data that makes it difficult to find what you are looking for. Even
though XML documents are very structured, they are best suited for
machines. Most people find it far easier to view an HTML report. A
portion of the response from the XigniteQuotes service is shown in
Listing 8.
It is important to note that users do not have to write this script
themselves. Using a generic XSLT script, the tool automatically
generates the script for this particular Web service from the WSDL
file. This generic script knows how to traverse a WSDL file and
extract the structure of the response message. It is aware of the XML
Schema elements such as ComplexType, Sequence, All, Group, minOccurs,
maxOccurs, etc. Using this information it infers when to display
items as text fields and when to create tables.
The reporting process can be summarized as follows:
1. WSDL file for Web service + Generic XSLT Script = XSLT script
for the Web service
2. Web service response in XML + XSLT script for the Web service
= HTML report
The generic script processes the entire response and presents it in
an HTML format, which may turn out to be very verbose. Because of
this the tool displays the schema of the response, so users can
visually select the elements of interest. This will generate a
smaller XSLT script that will extract only the pertinent information
from the response and present it as a report.
The generic script can be extended using richer XSLT semantics and
formatting objects. This will enable us to transform the response
into any format, including HTML, PDF, Word, or another XML document
(see Figure 1).
Conclusion
Although our sample client is written using the Java language and
Java-based APIs, a similar application can be written using tools and
APIs available in Microsoft's .NET environment. The fact that WSDL
doesn't force a particular programming model is actually one of the
beauties of Web services.
WSClient is a useful mechanism for trying Web services. As we have
seen you can use it to invoke any Web service and generate
easy-to-read reports based on the response.
Our tool also can be used as a first step towards integrating Web
services. Users can create a named scenario by specifying a WSDL
file, the input XML (which is configurable), and an optional XSLT
script. Through an API, the tool can be programmatically instructed
to run the scenario identified by its name and return the result.
This localizes the complexity of dealing with Web services in the
tool and encapsulates the complexity from the invoking program. If
used properly, this tool can help you make the transition from
tentative evaluation of Web services to their complete adoption.
Author Bio
Bikash Behera and Jim Winfield are senior software architects at
Metaserver, Inc., a provider of business process integration software.
Building Your Own SOAP Client and Reporting Tool by Bikash Behera & Jim Winfield
WSJ Vol 03 Issue 10 - pg.52
Listing 1.
public List buildComponents(String wsdlURI)
{
// The list of components that will be returned
List serviceList = Collections.synchronizedList(new ArrayList());
// Create the WSDL Reader object
WSDLReader reader = wsdlFactory.newWSDLReader();
try
{
// Read the WSDL and get the top-level Definition object
Definition def = reader.readWSDL(null, wsdlURI);
// Create a castor schema from the types element defined in WSDL
// This method will return null if there are types defined in the WSDL
wsdlTypes = createSchemaFromTypes(def);
// Get the services defined in the document
Map services = def.getServices();
if(services != null)
{
// Create a component for each service defined
Iterator svcIter = services.values().iterator();
for(int i = 0; svcIter.hasNext(); i++)
{
// Create a new ServiceInfo component for each service found
ServiceInfo serviceInfo = new ServiceInfo();
// Populate the new component from the WSDL Definition read
populateComponent(serviceInfo, (Service)svcIter.next());
// Add the new component to the List to be returned
serviceList.add(serviceInfo);
}
}
}
catch(Throwable t)
{
// Process the error/exception
System.err.println(t.getMessage());
}
// return the List of services we created
return serviceList;
}
/**
* Populates a ServiceInfo instance from the specified Service definiition
*
* @param component The component to populate
* @param service The Service to populate from
*
* @return The populated component is returned representing the Service parameter
*/
private ServiceInfo populateComponent(ServiceInfo component, Service service)
{
// Get the qualified service name information
QName qName = service.getQName();
// Get the service's namespace URI
String namespace = qName.getNamespaceURI();
// Use the local part of the qualified name for the component's name
String name = qName.getLocalPart();
// Set the name
component.setName(name);
// Get the defined ports for this service
Map ports = service.getPorts();
// Use the Ports to create methods for all request/response messages defined
Iterator portIter = ports.values().iterator();
while(portIter.hasNext())
{
// Get the next defined port
Port port = (Port)portIter.next();
// Get the Port's Binding
Binding binding = port.getBinding();
// Now we will create operations from the Binding information
List operations = buildOperations(binding);
// Process methods built from the binding information
Iterator operIter = operations.iterator();
while(operIter.hasNext())
{
OperationInfo operation = (OperationInfo)operIter.next();
// Set the namespace URI for the method using the service's namespace.
operation.setNamespaceURI(namespace);
// Find the SOAP target URL
ExtensibilityElement addrElem =
findExtensibilityElement(port.getExtensibilityElements(), "address");
if(addrElem != null && addrElem instanceof SOAPAddress)
{
// Set the SOAP target URL
SOAPAddress soapAddr = (SOAPAddress)addrElem;
operation.setTargetURL(soapAddr.getLocationURI());
}
// Add the method to the component
component.addOperation(operation);
}
}
return component;
}
Listing 2.
// Get the operation bound to the binding operation
Operation operation = bindingOperation.getOperation();
// From the operation we can get the Input definition
Input inDef = operation.getInput();
// Get the message defintion for the Input
Message inMessage = inDef.getMessage();
// Get the parts defined for this message
Map parts = inMessage.getParts();
Listing 3.
<types xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:s0="http://www.xignite.com/services/">
<s:schema elementFormDefault="qualified"
targetNamespace="http://www.xignite.com/services/">
<s:element name="GetQuotesHistorical">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="Symbol" type="s:string" />
<s:element minOccurs="1" maxOccurs="1" name="Month" type="s:int" />
<s:element minOccurs="1" maxOccurs="1" name="Year" type="s:int" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetQuotesHistoricalResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="GetQuotesHistoricalResult"
type="s0:ArrayOfQuote" />
</s:sequence>
</s:complexType>
</s:element>
<s:complexType name="ArrayOfQuote">
<s:sequence>
<s:element minOccurs="0" maxOccurs="unbounded" name="Quote" nillable="true"
type="s0:Quote" />
</s:sequence>
</s:complexType>
<s:complexType name="Quote">
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="Error" type="s:string" />
<s:element minOccurs="0" maxOccurs="1" name="Symbol" type="s:string" />
<s:element minOccurs="0" maxOccurs="1" name="Date" type="s:string" />
<s:element minOccurs="1" maxOccurs="1" name="Open" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="High" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="Low" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="Last" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="LastAdjusted" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="PercentChange" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="Volume" type="s:double" />
<s:element minOccurs="1" maxOccurs="1" name="Change" type="s:double" />
</s:sequence>
</s:complexType>
</s:schema>
</types>
Listing 4
/**
* Builds and adds parameters to the supplied method given a SOAP Message definition
(from WSDL)
*
* @param method The method that the parameters built will be added to
* @param msg The SOAP Message definition that has parts to defined
parameters for
*/
private String buildMessageText(OperationInfo operationInfo, Message msg)
{
// Build a JDOM representation of the message
//Element rootElem = new Element(operationInfo.getTargetMethodName(),
operationInfo.getTargetObjectURI());
StringBuffer sb = new StringBuffer();
// Get the message parts
List msgParts = msg.getOrderedParts(null);
// Process each part
Iterator iter = msgParts.iterator();
while(iter.hasNext())
{
// Get each part
Part part = (Part)iter.next();
// Add content for each message part
Element partElem = new Element(part.getElementName().getLocalPart());
// Determine if this message part's type is complex
XMLType xmlType = getXMLType(part);
if(xmlType != null && xmlType.isComplexType())
{
// Build the message structure
buildComplexPart((ComplexType)xmlType, partElem);
}
else
{
partElem.addContent("0");
}
sb.append(XMLSupport.outputString(partElem));
}
return sb.toString();
}
Listing 5.
// All SAAJ connections are created by using a connection factory
SOAPConnectionFactory conFactory = SOAPConnectionFactory.newInstance();
SOAPConnection connection = conFactory.createConnection();
// All SAAJ SOAP messages are created by using a message factory
MessageFactory msgFactory = MessageFactory.newInstance();
SOAPMessage msg = msgFactory.createMessage();
Listing 6.
// Get the SOAP part from the SOAP message object
SOAPPart soapPart = msg.getSOAPPart();
// The SOAP part object will automatically contain the SOAP envelope
SOAPEnvelope envelope = soapPart.getEnvelope();
if(operation.getStyle().equalsIgnoreCase("rpc"))
{
// Add namespace declarations to the envelope
// usually only required for RPC/encoded
envelope.addNamespaceDeclaration
(XSI_NAMESPACE_PREFIX, XSI_NAMESPACE_URI);
envelope.addNamespaceDeclaration
(XSD_NAMESPACE_PREFIX, XSD_NAMESPACE_URI);
}
// The client does not yet support SOAP headers
SOAPHeader header = envelope.getHeader();
header.detachNode();
// Get the SOAP body from the envelope and populate it
SOAPBody body = envelope.getBody();
// Create the default namespace for the SOAP body
body.addNamespaceDeclaration("", operation.getNamespaceURI());
// Add the service information
String targetObjectURI = operation.getTargetObjectURI();
if(targetObjectURI == null)
{
// The target object URI should not be null
targetObjectURI = "";
}
// Add the service information
Name svcInfo = envelope.createName(operation.getTargetMethodName(), "",
targetObjectURI);
// Add the service information
SOAPElement svcElem = body.addChildElement(svcInfo);
if(operation.getStyle().equalsIgnoreCase("rpc"))
{
// Set the encoding style of the service element
svcElem.setEncodingStyle(operation.getEncodingStyle());
}
// Add the message to the SOAP body
svcElem.addTextNode(operation.getInputMessageText());
// Check for a SOAPAction
String soapActionURI = operation.getSoapActionURI();
// Add the SOAPAction value as a MIME header
MimeHeaders mimeHeaders = msg.getMimeHeaders();
mimeHeaders.setHeader("SOAPAction", "\"" +
operation.getSoapActionURI() + "\"");
// Save changes to the message we just populated
msg.saveChanges();
Listing 7
// Get ready for the invocation
URLEndpoint endpoint = new URLEndpoint(operation.getTargetURL());
// Make the call
SOAPMessage response = connection.call(msg, endpoint);
// Close the connection, we are done with it
connection.close();
// Get the content of the SOAP response
Source responseContent = response.getSOAPPart().getContent();
// Convert the SOAP response into a JDOM
TransformerFactory tFact = TransformerFactory.newInstance();
Transformer transformer = tFact.newTransformer();
JDOMResult jdomResult = new JDOMResult();
transformer.transform(responseContent, jdomResult);
// Get the document created by the transform operation
Document responseDoc = jdomResult.getDocument();
// Send the response to the console
String strResponse = XMLSupport.outputString(responseDoc);
System.out.println("SOAP Response from: " + operation.getTargetMethodName() +
": " + strResponse);
// Set the response as the output message
operation.setOutputMessageText(strResponse);
Listing 8.
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetQuotesHistoricalResponse xmlns="http://www.xignite.com/services/">
<GetQuotesHistoricalResult>
<Quote>
<Date>12/31/2002</Date>
<Open>3.07</Open>
<High>3.15</High>
<Low>3.04</Low>
<Last>3.11</Last>
<LastAdjusted>3.11</LastAdjusted>
<PercentChange>0.974</PercentChange>
<Volume>39579300</Volume>
<Change>0.03</Change>
</Quote>
<Quote>
<Date>12/30/2002</Date>
<Open>3.28</Open>
<High>3.31</High>
<Low>3.05</Low>
<Last>3.08</Last>
<LastAdjusted>3.08</LastAdjusted>
<PercentChange>-5.231</PercentChange>
<Volume>39969900</Volume>
<Change>-0.17</Change>
</Quote>
…
</GetQuotesHistoricalResult>
</GetQuotesHistoricalResponse>
</soap:Body>
</soap:Envelope
With the help of an automatically generated XSLT script, we can transform the XML
response into an HTML report.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method = "html"/>
…………
<xsl:template match = "GetQuotesHistoricalResult">
<html>
<head>
</head>
<body>
<table width="550" border="2" align="center">
<tr valign="bottom">
<td width="150" align="center">Date</td>
<td width="150" align="center">Open</td>
<td width="150" align="center">High</td>
<td width="150" align="center">Low</td>
<td width="150" align="center">Last</td>
</tr>
<xsl:apply-templates/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="Quote">
<tr>
<td align="left" width="150"><xsl:value-of select="Date"/></td>
<td align="left" width="150"><xsl:value-of select="Open"/></td>
<td align="left" width="150"><xsl:value-of select="High"/></td>
<td align="left" width="150"><xsl:value-of select="Low"/></td>
<td align="left" width="150"><xsl:value-of select="Last"/></td>
</tr>
</xsl:template>
</xsl:stylesheet>
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com