If you are depressed and skeptical by now, read on. A lot of great work is being developed that is producing positive results.
The SOAPBuilders group was created to promote interoperability as the SOAP specification matures. The SOAPBuilders discussion group provides a forum for SOAP implementers to discuss issues related to interoperability and the various specifications. It has existed since January 2001. The group has a community of over 750 members and is highly active.
Through SOAPBuilders, two labs for interoperability testing against SOAP 1.1 have been developed. The test labs provide test suites that can be used by SOAP implementations to test interoperability. The SOAPBuilders test suite was initially based on tests created to improve interoperability between Apache SOAP and SOAP::Lite. The tests are a collection of SOAP-RPC echo invocations. WSDL documents and browser-based clients for using the service are provided for all test suites. A directory of endpoints for each implementation is maintained on the lab site.
XMethods (http://www.xmethods.net/ilab/) maintained Round 1, conducted in June 2001. The SOAPBuilders interoperability activity is now in Round 2, maintained by Bob Cunnings of White Mesa (http://www.whitemesa.com/interop.htm). For each round, XMethods and White Mesa provide lists of implementations and WSDL documents that have been tested and document the test results. Based on this information, you can determine the state of interoperability for a specific implementation.
Most implementations listed for Round 1 and many listed for initial testing during Round 2 have released updates to correct identified interoperability issues. Many listed Round 1 services are no longer available for testing. The Round 2 tests are better maintained, but don’t support Apache SOAP 2.2 clients because that tool generates an older schema reference in the SOAP envelope. We will show how to fix this problem later in this chapter; Axis (the next generation of Apache SOAP) does not have this problem.
The Round 1 Interoperability Lab included specifications for a Service test suite and a Service via SOAP Intermediary test suite.
The Standard Round 2 Interoperability Lab has been expanded to include specifications for a Base test suite, a Group B test suite, an echo Header or Group C test suite, a Service via SOAP Intermediary test suite (which duplicates the methods from the Base suite), Digest Authentication Implementation, Web Services Routing Protocol (for intermediary nodes as well), and Document/Literal SOAP Operations.
The
Base
test suite
includes methods to test types (string, string array, integer,
integer array, float, float array, struct, and struct
array). Each method accepts the specific type
and echoes it back to the client. Explicit typing information can be
carried on the wire, but should not be required on incoming messages.
For example, here is the
echoString
method client request (with
sample envelopes). It accepts a string as an argument:
POST /interop HTTP/1.0 Host: www.whitemesa.net User-Agent: White Mesa SOAP Interop Client/1.0 Content-Type: text/xml; charset="utf-8" Content-Length: 502 SOAPAction: "http://soapinterop.org/" <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 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> <m:echoString xmlns:m="http://soapinterop.org/"> <inputString>hello world</inputString> </m:echoString> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Here is a successful server response; as you’d expect, it returns the same string:
HTTP/1.0 200 OK Date: Wed, 20 Jun 2001 02:44:16 GMT Server: WhiteMesa SOAP Server/2.3 Content-Type: text/xml; charset="utf-8" Content-Length: 508 <?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 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> <m:echoStringResponse xmlns:m="http://soapinterop.org/"> <return>hello world</return> </m:echoStringResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Similar echoing methods are provided for:
echoVoid
Accepts no arguments and returns no arguments
echoBase64
Accepts a binary object and echoes it back
echoHexBinary
Accepts a hex-encoded object and echoes it back
echoDate
Accepts a Date/Time and echoes it back
echoDecimal
Accepts a decimal and echoes it back
echoBoolean
Accepts a Boolean and echoes it back
The Group B test suite includes methods with more complex serialization requirements. It includes these echo methods:
echoStructAsSimpleTypes
Accepts a single struct and echoes it (decomposed into three output parameters)
echoSimpleTypesAsStruct
Accepts three input parameters and echoes them integrated into a single struct
echo2DStringArray
Accepts a single two-dimensional array of type
xsd:string
and echoes it
echoNestedStruct
Accepts a single struct with a nested struct type member and echoes it
echoNestedArray
Accepts a single struct with a nested array type member and echoes it back
The echoHeader or Group C
test suite offers standard headers used to test header processing.
The SOAP body contains an echoVoid
method call.
The message recipient is the default actor, and the
mustUnderstand
element value can be either 0 or 1
to test conformance to the SOAP 1.1 specification. The following
header entries are provided:
echoMeStringRequest
Contains a string that is echoed in a corresponding response header
entry (echoMeStringResponse
)
echoMeStructRequest
Contains a single, echoed struct
(echoMeStructResponse
)
Any header that is not understood by the server. If a server cannot
process the header and mustUnderstand
is set to
true
(1
), the server must issue
a SOAP Fault if it is the target of the header entry, based on the
value of the actor attribute. If the target is another server, the
server can ignore the header entry. If a server cannot process the
header, and mustUnderstand
is set to
false
(0
), the server can
ignore the header entry and is not required to fault (regardless of
whether it is the targeted server).
Polymorphic type methods are also
available. They include: echoPolyMorph
,
echoPolyMorphArray
, and
echoPolyMorphStruct
.
Round 3 of interoperability testing, scheduled for the end of February 2002, is co-hosted by IONA and Microsoft and will focus on WSDL interoperability. A formal plan is not yet finalized, but proposed testing includes the following success criteria:
Tools can generate WSDL documents correctly for designated scenarios and consume WSDLs generated by other tools.
Tools can consume and reuse WSDL documents.
Discussion topics that have been most active on the SOAPBuilders list will be included in the tests:
Import (schema import)
Document/literal services with multiple schema
SOAP binding interoperability issues
Let’s examine the EchoTestClient that comes with Apache SOAP 2.2. We picked Apache SOAP because we already explained its underpinnings in earlier chapters and wanted to focus on the test itself. The workings of the test, the interoperability issues, and the concepts of encoding and serialization are generic enough to apply to any SOAP infrastructure you may use. The EchoTestClient represents a Java implementation of the SOAPBuilders Interoperability Labs “Round 1” test and can also be used for the “Round 2: Base” interoperability tests. It can be found in the soap-2.2samplesinterop directory, or as part of the examples available on this book’s web site (http://www.oreilly.com/catalog/javawebserv).
We also verified some of the test results using the Apache Axis version of the same test and used other third-party visual interfaces. These interfaces will be shown later.
To see how these tests work, we need an Internet connection. Let’s run the Apache SOAP 2.2 EchoTestClient against the Iona XMLBus echo test service, which Iona has hosted as part of their participation in the SB Round 2 effort:
java samples.interop.EchoTestClient http://interop.xmlbus.com:7002/xmlbus/container/ InteropTest/BaseService/BasePort
The command must be typed on a single line. If you use the
EchoTestClient
straight from the Apache SOAP
distribution, you should see the following errors as output:
echoInteger generated fault: Fault Code = SOAP-ENV:Server Fault String = xsi:type doesn't match. Expected http://www.w3.org/2001/ XMLSchema:int but found http://www.w3.org/1999/XMLSchema:int echoFloat generated fault: Fault Code = SOAP-ENV:Server Fault String = xsi:type doesn't match. Expected http://www.w3.org/2001/ XMLSchema:float but found http://www.w3.org/1999/XMLSchema:float soapAction: http://soapinterop.org/ echoString generated fault: Fault Code = SOAP-ENV:Server Fault String = xsi:type doesn't match. Expected http://www.w3.org/2001/ XMLSchema:string but found http://www.w3.org/1999/XMLSchema:string soapAction: http://soapinterop.org/ ...
This result illustrates one of the most basic interoperability problems. The receiving service expected the schema definition http://www.w3.org/2001/XMLSchema, but the sending client generated a SOAP envelope based on http://www.w3.org/1999/XMLSchema. We will show how simple it is to fix this problem in a moment, when we get to the code. In the meantime, when the EchoTestClient works correctly, its output looks like this:
echoInteger OK echoFloat OK echoString OK echoStruct OK echoIntegerArray OK echoFloatArray OK echoStringArray OK echoStructArray OK
This test may look simple, but quite a bit is going on internally. At the highest level, this test makes eight RPC calls,[15] each with its own unique datatype as a parameter. The receiving method simply takes that parameter and returns it as the method’s return value. The sending client then compares the returned data with the sent data to verify that it is the same. If the data are equal, we know that:
The sending client successfully packaged the method invocation and its parameters and correctly marshalled it onto the wire as a valid SOAP request.
The receiving SOAP processor received the request, identified that there was enough correct information in the SOAP request, and dispatched it to the appropriate service method.
The receiving SOAP processor also (optionally) validated the request against the WSDL that described the service.
The service method was invoked with the correct data and returned the data intended for the caller as the return value.
The server implementation correctly marshaled the return values onto the wire as a response envelope.
The sending client’s runtime infrastructure successfully unmarshaled the SOAP response envelope into the appropriate Java object.
The sent and returned data are equivalent.
The next exercise directs the EchoTestClient
at
our simple servlet from Chapter 3:
java samples.interop.EchoTestClient http://localhost:8080/examples/servlet/ SimpleHTTPReceive
You will see several errors in the sending client’s
console because the simple servlet is not set up to accommodate RPC
calls—but that’s not important for this
discussion. What’s important is that you should see
the following output in the Tomcat console window. The raw SOAP
envelopes that were generated by the
EchoTestClient
are shown here:
____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 469 ----------------------- <?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:echoInteger xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputInteger xsi:type="xsd:int">5</inputInteger> </ns1:echoInteger> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 466 ----------------------- <?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:echoFloat xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputFloat xsi:type="xsd:float">55.5</inputFloat> </ns1:echoFloat> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 476 ----------------------- <?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:echoString xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputString xsi:type="xsd:string">Hi there!</inputString> </ns1:echoString> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 656 ----------------------- <?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:echoStruct xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputStruct xmlns:ns2="http://soapinterop.org/xsd" xsi:type="ns2:SOAPStruct"> <varInt xsi:type="xsd:int">5</varInt> <varFloat xsi:type="xsd:float">10.0</varFloat> <varString xsi:type="xsd:string">Hola, baby</varString> </inputStruct> </ns1:echoStruct> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 748 ----------------------- <?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:echoIntegerArray xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputIntegerArray xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:Array" ns2:arrayType="xsd:int[5]"> <item xsi:type="xsd:int">5</item> <item xsi:type="xsd:int">4</item> <item xsi:type="xsd:int">3</item> <item xsi:type="xsd:int">2</item> <item xsi:type="xsd:int">1</item> </inputIntegerArray> </ns1:echoIntegerArray> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 762 ----------------------- <?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:echoFloatArray xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputFloatArray xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:Array" ns2:arrayType="xsd:float[5]"> <item xsi:type="xsd:float">5.5</item> <item xsi:type="xsd:float">4.4</item> <item xsi:type="xsd:float">3.3</item> <item xsi:type="xsd:float">2.2</item> <item xsi:type="xsd:float">1.1</item> </inputFloatArray> </ns1:echoFloatArray> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 801 ----------------------- <?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:echoStringArray xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputStringArray xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:Array" ns2:arrayType="xsd:string[5]"> <item xsi:type="xsd:string">First</item> <item xsi:type="xsd:string">Second</item> <item xsi:type="xsd:string">Fifth (just kidding :))</item> <item xsi:type="xsd:string">Fourth</item> <item xsi:type="xsd:string">Last</item> </inputStringArray> </ns1:echoStringArray> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________ ____________________________ Received request. ----------------------- SOAPAction = "http://soapinterop.org/" Host = localhost Content-Type = text/xml; charset=utf-8 Content-Length = 1527 ----------------------- <?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:echoStructArray xmlns:ns1="http://soapinterop.org/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputStructArray xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:Array" xmlns:ns3="http://soapinterop.org/xsd" ns2:arrayType="ns3:SOAPStruct[5]"><item xsi:type="ns3:SOAPStruct"> <varInt xsi:type="xsd:int">5</varInt> <varFloat xsi:type="xsd:float">5.55555</varFloat> <varString xsi:type="xsd:string">cinqo</varString> </item> <item xsi:type="ns3:SOAPStruct"> <varInt xsi:type="xsd:int">4</varInt> <varFloat xsi:type="xsd:float">4.4444</varFloat> <varString xsi:type="xsd:string">quattro</varString> </item> <item xsi:type="ns3:SOAPStruct"> <varInt xsi:type="xsd:int">3</varInt> <varFloat xsi:type="xsd:float">3.333</varFloat> <varString xsi:type="xsd:string">tres</varString> </item> <item xsi:type="ns3:SOAPStruct"> <varInt xsi:type="xsd:int">2</varInt> <varFloat xsi:type="xsd:float">2.22</varFloat> <varString xsi:type="xsd:string">duet</varString> </item> <item xsi:type="ns3:SOAPStruct"> <varInt xsi:type="xsd:int">1</varInt> <varFloat xsi:type="xsd:float">1.1</varFloat> <varString xsi:type="xsd:string">un</varString> </item> </inputStructArray> </ns1:echoStructArray> </SOAP-ENV:Body> </SOAP-ENV:Envelope> ____________________________
We will examine some of this output in more detail as we walk through the example. But first, here is the Apache SOAP 2.2 EchoTestClient in its entirety:
package samples.interop; import java.util.Vector; import org.apache.soap.*; import org.apache.soap.encoding.SOAPMappingRegistry; import org.apache.soap.encoding.soapenc.*; import org.apache.soap.rpc.*; import org.apache.soap.messaging.*; import java.net.URL; import org.apache.soap.util.xml.*; import java.io.*; import org.w3c.dom.*; import org.apache.soap.util.*; import java.lang.reflect.*; /** A quick-and-dirty client for the Interop echo test services as defined * at http://www.xmethods.net/ilab. * * Defaults to the Apache endpoint, but you can point it somewhere else via * the command line: * * EchoTestClient http://some.other.place/ * * DOES NOT SUPPORT DIFFERENT SOAPACTION URIS YET. * * @author Glen Daniels ([email protected]) */ public class EchoTestClient { SOAPMappingRegistry smr = new SOAPMappingRegistry(Constants.NS_URI_CURRENT_SCHEMA_XSD); // public static final String DEFAULT_URL = // "http://nagoya.apache.org:5089/soap/servlet/rpcrouter"; public static final String DEFAULT_URL = "http://localhost:8080/soap/servlet/rpcrouter"; // pick one! First line works for round 1; second for round 2: base public static final String ACTION_URI = "urn:soapinterop"; // public static final String ACTION_URI = "http://soapinterop.org/"; public static final String OBJECT_URI = "http://soapinterop.org/xsd"; public Header header = null; public static void main(String args[]) { URL url = null; try { if (args.length > 0) { url = new URL(args[0]); } else { url = new URL(DEFAULT_URL); } } catch (Exception e) { e.printStackTrace( ); } EchoTestClient eTest = new EchoTestClient( ); eTest.doWork(url); } private static boolean equals(Object obj1, Object obj2) { if (obj1 == null) return (obj2 == null); if (obj1.equals(obj2)) return true; if (!obj2.getClass().isArray( )) return false; if (!obj1.getClass().isArray( )) return false; if (Array.getLength(obj1) != Array.getLength(obj2)) return false; for (int i=0; i<Array.getLength(obj1); i++) if (!equals(Array.get(obj1,i),Array.get(obj2,i))) return false; return true; } public void doWork(URL url) { IntDeserializer intDser = new IntDeserializer( ); FloatDeserializer floatDser = new FloatDeserializer( ); StringDeserializer stringDser = new StringDeserializer( ); ArraySerializer arraySer = new ArraySerializer( ); DataSerializer dataSer = new DataSerializer( ); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName(OBJECT_URI, "SOAPStruct"), Data.class, dataSer, dataSer); Integer i = new Integer(5); Parameter p = new Parameter("inputInteger", Integer.class, i, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, intDser); doCall(url, "echoInteger", p); p = new Parameter("inputFloat", Float.class, new Float(55.5), null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, floatDser); doCall(url, "echoFloat", p); p = new Parameter("inputString", String.class, "Hi there!", null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, stringDser); doCall(url, "echoString", p); p = new Parameter("inputStruct", Data.class, new Data(5, "Hola, baby", (float)10.0), null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, dataSer); doCall(url, "echoStruct", p); p = new Parameter("inputIntegerArray", Integer[].class, new Integer[]{ new Integer(5), new Integer(4), new Integer(3), new Integer(2), new Integer(1)}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoIntegerArray", p); p = new Parameter("inputFloatArray", Float[].class, new Float[]{ new Float(5.5), new Float(4.4), new Float(3.3), new Float(2.2), new Float(1.1)}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoFloatArray", p); p = new Parameter("inputStringArray", String[].class, new String[]{ "First", "Second", "Fifth (just kidding :))", "Fourth", "Last"}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoStringArray", p); p = new Parameter("inputStructArray", Data[].class, new Data[]{ new Data(5, "cinqo", new Float("5.55555").floatValue( )), new Data(4, "quattro", (float)4.4444), new Data(3, "tres", (float)3.333), new Data(2, "duet", (float)2.22), new Data(1, "un", (float)1.1)}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoStructArray", p); } public void doCall(URL url, String methodName, Parameter param) { try { Call call = new Call( ); Vector params = new Vector( ); params.addElement(param); call.setSOAPMappingRegistry(smr); call.setTargetObjectURI(ACTION_URI); call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC); call.setMethodName(methodName); call.setParams(params); if (header != null) call.setHeader(header); String soapAction = ACTION_URI; // System.out.println("soapAction: " + soapAction); /* if (true) { soapAction = soapAction + methodName; } */ Response resp = call.invoke(url, soapAction); // check response if (!resp.generatedFault( )) { Parameter ret = resp.getReturnValue( ); Object output = ret.getValue( ); Object input = param.getValue( ); if (equals(input,output)) { System.out.println(methodName + " OK"); } else { System.out.println(methodName + " FAIL: " + output); } } else { Fault fault = resp.getFault ( ); System.err.println (methodName + " generated fault: "); System.out.println (" Fault Code = " + fault.getFaultCode( )); System.out.println (" Fault String = " + fault.getFaultString( )); } } catch (Exception e) { e.printStackTrace( ); } } }
Let’s examine some of this code in detail. First, here is what we did to get around the mismatch of the schema namespace URIs. The value passed into the mapping registry is based on a default, which was initialized to use the older value:
public class EchoTestClient { SOAPMappingRegistry smr = new SOAPMappingRegistry(Constants.NS_URI_CURRENT_SCHEMA_XSD);
Apache SOAP 2.2 supports the 2001 schema—it just does not
default to it. We could have simply changed this line of code, but
changing it doesn’t fix the problem everywhere.
Instead, we got the Constants.java file from the
Apache SOAP distribution; updated
Constants.NS_URI_CURRENT_SCHEMA_XSD
to default to
the desired value; and recompiled the Constants
,
SoapEncUtils
, and
ArraySerializer
classes:
/* Changed this: public static final String NS_URI_CURRENT_SCHEMA_XSI = NS_URI_1999_SCHEMA_XSI; public static final String NS_URI_CURRENT_SCHEMA_XSD = NS_URI_1999_SCHEMA_XSD; */ // To this: public static final String NS_URI_CURRENT_SCHEMA_XSI = NS_URI_2001_SCHEMA_XSI; public static final String NS_URI_CURRENT_SCHEMA_XSD = NS_URI_2001_SCHEMA_XSD;
We also commented out the following lines in the doWork( )
method
of the test:
String soapAction = ACTION_URI; /* if (true) { soapAction = soapAction + methodName; } */ Response resp = call.invoke(url, soapAction);
Almost every test we tried did not like having the method name
appended to the soapAction
header.
It’s good that the author of the code was aware of
this problem and isolated it so it could be turned on and off easily.
Also, depending on what round of tests we wanted to run, we had to
change the action URI; the Round 1 tests expected the URI
urn:soapinterop, while the current Round 2: base
tests expect http://soapinterop.org/:
// pick one! First line works for round 1; second for round 2: base public static final String ACTION_URI = "urn:soapinterop"; // public static final String ACTION_URI = "http://soapinterop.org/";
The Apache Axis version of the same test didn’t need any modification because it is a new test that presumably works only against the Round 2 tests.
The mapping registry maintains the relationship between the XML datatypes and their corresponding Java types. It is also a utility that marshals and unmarshals the data between the Java representation and the SOAP envelope representation. It accomplishes this task by maintaining a list of serialization and deserialization classes for each datatype it knows about.
By default, the
SOAPMappingRegistry
is instantiated with the appropriate mappings and
serializers/deserializers for each type that it supports.
The default serialization mapping for the String
class creates the XML rendition. The SOAP envelope resulting from
this call looks like:
<?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:echoString xmlns:ns1="urn:soapinterop" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputString xmlns:ns2="http://www.w3.org/2001/XMLSchema" xsi:type="ns2:string"> Hi there! </inputString> </ns1:echoString> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Note the inclusion of the xsi:type
attribute. Not
all SOAP toolkits that participated in the Round 1 testing supported
this attribute.
These default mappings may be overridden by calling the
mapTypes( )
method. The mapTypes( )
method takes the following parameters:
A namespace for the schema
A QName for the element that represents the parameter/return value[16]
An instance of a class that describes the data
An instance of an object that performs the serialization of the Java object into the XML representation
An instance of an object that deserializes the XML data back into the Java object
For example, the following objects are part of Apache
SOAP’s serialization framework and are included in
the org.apache.soap.encoding.soapenc
package:
IntDeserializer intDser = new IntDeserializer( ); FloatDeserializer floatDser = new FloatDeserializer( ); StringDeserializer stringDser = new StringDeserializer( ); ArraySerializer arraySer = new ArraySerializer( );
In the EchoTestClient, the call to mapTypes( )
is
necessary for the “Return” because
the SOAP Toolkit that services the request may not support the
placement of the xsi:type
attribute in the
response envelope. The type information for the parameter is
specified along with its name and value when the parameter object is
constructed:
p = new Parameter("inputString", String.class, "Hi there!", null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, stringDser); doCall(url, "echoString", p);
Default mappings are available for more complicated objects, such as arrays. The following code constructs a method invocation with a parameter that is an array of 5 integers, with the values of 1 through 5:
p = new Parameter("inputIntegerArray", Integer[].class, new Integer[]{ new Integer(5), new Integer(4), new Integer(3), new Integer(2), new Integer(1)}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoIntegerArray", p);
The serialization framework knows how to traverse the array, using
the appropriate serialization classes for each member element (in
this case, all of type Integer
). The serialized
output looks like:
<?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:echoIntegerArray xmlns:ns1="urn:soapinterop" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputIntegerArray xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="ns2:Array" xmlns:ns3="http://www.w3.org/2001/XMLSchema" ns2:arrayType="ns3:int[5]"> <item xsi:type="ns3:int">5</item> <item xsi:type="ns3:int">4</item> <item xsi:type="ns3:int">3</item> <item xsi:type="ns3:int">2</item> <item xsi:type="ns3:int">1</item> </inputIntegerArray> </ns1:echoIntegerArray> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Part of the test involves sending a complex data structure represented as a SOAP struct.[17] The structure contains three data items: an integer, a string, and a float. Creating a custom data serialization requires the following steps:
Create a Java object to describe the data.
Create a custom class to perform the serialization/deserialization.
Plug them into the mapping registry.
In the following code,
DataSerializer
is a custom class that we will explain
shortly. The custom object, the serializer, and the deserializer are
plugged into the mapping registry with the call to mapTypes( )
:
DataSerializer dataSer = new DataSerializer( ); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName(OBJECT_URI, "SOAPStruct"), Data.class, dataSer, dataSer);
As shown in the following listing, the setup for call( )
is similar to the setup that was used for the
“built-in” types:
p = new Parameter("inputStruct", Data.class, new Data(5, "Hola, baby", (float)10.0), null); doCall(url, "echoStruct", p);
Here’s the SOAP envelope generated by the call:
<?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:echoStruct xmlns:ns1="urn:soapinterop" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <inputStruct xmlns:ns2="http://soapinterop.org/xsd" xsi:type="ns2:SOAPStruct"> <varInt xmlns:ns3="http://www.w3.org/2001/XMLSchema" xsi:type="ns3:int"> 5 </varInt> <varFloat xmlns:ns4="http://www.w3.org/2001/XMLSchema" xsi:type="ns4:float"> 10.0 </varFloat> <varString xmlns:ns5="http://www.w3.org/2001/XMLSchema" xsi:type="ns5:string"> Hola, baby </varString> </inputStruct> </ns1:echoStruct> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
The next listing shows the Java class that represents the data
structure. It holds the data and implements an equals( )
method that the EchoTestClient
uses to
compare results:
class Data { Integer myInt; String myString; Float myFloat; public Data( ) { } public Data(int i, String s, float f) { myInt = new Integer(i); myString = s; myFloat = new Float(f); } public String toString( ) { return "Data[MyInt=" + myInt + ", MyString='" + myString + "', myFloat=" + myFloat + "]"; } /** * Equality comparison. */ public boolean equals(Object object) { if (!(object instanceof Data)) return false; Data that= (Data) object; if (!this.myInt.equals(that.myInt)) return false; if (!this.myFloat.equals(that.myFloat)) return false; if (this.myString == null) { if (that.myString != null) return false; } else { if (!this.myString.equals(that.myString)) return false; } return true; }; }
The custom
DataSerializer
class implements both the
Serializer
and Deserializer
interfaces; these interfaces define the marshall( )
and
unmarshall( )
methods, respectively. Putting both methods
in one class and using that class for both serialization and
deserialization is a common design strategy. The marshall( )
method simply calls the individual marshall( )
method for each datatype in the Java class. Likewise, the
unmarshall( )
method calls the individual
unmarshall( )
method for each type in the SOAP
struct and returns a JavaBean instance:
package samples.interop; import java.util.Vector; import org.apache.soap.*; import org.apache.soap.encoding.SOAPMappingRegistry; import org.apache.soap.encoding.soapenc.*; import org.apache.soap.rpc.*; import org.apache.soap.messaging.*; import java.net.URL; import org.apache.soap.util.xml.*; import java.io.*; import org.w3c.dom.*; import org.apache.soap.util.*; import java.lang.reflect.*; public class DataSerializer implements Serializer, Deserializer { public void marshall(String inScopeEncStyle, Class javaType, Object src, Object context, Writer sink, NSStack nsStack, XMLJavaMappingRegistry xjmr, SOAPContext ctx) throws IllegalArgumentException, IOException { if(!javaType.equals(Data.class)) { throw new IllegalArgumentException("Can only serialize Data instances"); } Data data = (Data)src; nsStack.pushScope( ); if(src!=null) { SoapEncUtils.generateStructureHeader(inScopeEncStyle, javaType, context, sink, nsStack,xjmr); sink.write(StringUtils.lineSeparator); xjmr.marshall(inScopeEncStyle, Integer.class, data.myInt, "varInt", sink, nsStack, ctx); sink.write(StringUtils.lineSeparator); xjmr.marshall(inScopeEncStyle, Float.class, data.myFloat, "varFloat", sink, nsStack, ctx); sink.write(StringUtils.lineSeparator); xjmr.marshall(inScopeEncStyle, String.class, data.myString, "varString", sink, nsStack, ctx); sink.write(StringUtils.lineSeparator); sink.write("</" + context + '>'), } else { SoapEncUtils.generateNullStructure(inScopeEncStyle, javaType, context, sink, nsStack,xjmr); } nsStack.popScope( ); } public Bean unmarshall(String inScopeEncStyle, QName elementType, Node src, XMLJavaMappingRegistry xjmr, SOAPContext ctx) throws IllegalArgumentException { Element root = (Element)src; String name = root.getTagName( ); if (SoapEncUtils.isNull(root)) { return new Bean(Data.class, null); } Data ret = new Data( ); NodeList list = root.getElementsByTagName("varInt"); if (list == null || list.getLength( ) == 0) { throw new IllegalArgumentException( "No 'varInt' Element (deserializing Data struct)"); } Element el = (Element)list.item(0); ret.myInt = new Integer(DOMUtils.getChildCharacterData(el)); list = root.getElementsByTagName("varFloat"); if (list == null || list.getLength( ) == 0) { throw new IllegalArgumentException( "No 'varFloat' Element (deserializing Data struct)"); } el = (Element)list.item(0); ret.myFloat = new Float(DOMUtils.getChildCharacterData(el)); list = root.getElementsByTagName("varString"); if (list == null || list.getLength( ) == 0) { throw new IllegalArgumentException( "No 'varString' Element (deserializing Data struct)"); } el = (Element)list.item(0); ret.myString = ((Text)el.getFirstChild()).getData( ); return new Bean(Data.class, ret); } }
Next, the custom structs are aggregated into an array:
p = new Parameter("inputStructArray", Data[].class, new Data[]{ new Data(5, "cinqo", new Float("5.55555").floatValue( )), new Data(4, "quattro", (float)4.4444), new Data(3, "tres", (float)3.333), new Data(2, "duet", (float)2.22), new Data(1, "un", (float)1.1)}, null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, arraySer); doCall(url, "echoStructArray", p); }
The output looks like . . . well, you get the idea. Because the
Data
type is already plugged into the
serialization framework, the default array handling mechanism can
just call Dataserializer
to do its work.
All this coding, marshalling, and unmarshalling is required because
we are using a dynamic call interface. On the service side, we
don’t need any of this work; we just make an RPC
call and Apache SOAP takes care of the rest. The server runtime needs
only the information specified in a deployment descriptor. The
EchoTest
Java service from Apache SOAP 2.2 is
comparatively uninteresting:
package samples.interop; /** An implementation of the interop echo service as defined at * http://www.xmethods.net/ilab. * * @author Glen Daniels ([email protected]) */ public class EchoTestService { public void nop( ) { } public int echoInteger(int i) { return i; } public float echoFloat(float f) { return f; } public String echoString(String str) { return str; } public Data echoStruct(Data data) { return data; } public int [] echoIntegerArray(int [] ii) { return ii; } public float [] echoFloatArray(float [] ff) { return ff; } public String [] echoStringArray(String [] ss) { return ss; } public Data [] echoStructArray(Data [] ds) { return ds; } }
In an environment in which the client and service interfaces are automatically generated by a tool, such as one that implements JAX-RPC, this coding is moot because it is hidden from the developer. When you need to understand interoperability issues between clients and services, however, it is critical to understand the concepts behind them. In the future, we may reach a state in which interoperability “just works” between almost any client and server-side implementations; however, that future is still remote.
We ran a couple of other tests as well. We tried pointing the Apache SOAP client at the Round 2: Base tests for MS SOAP Toolkit Version 3.0 and got the following results. The first three invocations failed, and the other five succeeded:
java EchoTestClient http://mssoapinterop.org/stkV3/Interop.wsdl [SOAPException: faultCode=SOAP-ENV:Client; msg=No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/soap/ encoding/'.; targetException=java.lang.IllegalArgumentException: No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/ soap/encoding/'.] at org.apache.soap.rpc.Call.invoke(Call.java:246) at EchoTestClient.doCall(EchoTestClient.java:222) at EchoTestClient.doWork(EchoTestClient.java:142) at EchoTestClient.main(EchoTestClient.java:114) [SOAPException: faultCode=SOAP-ENV:Client; msg=No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/soap/ encoding/'.; targetException=java.lang.IllegalArgumentException: No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/ soap/encoding/'.] at org.apache.soap.rpc.Call.invoke(Call.java:246) at EchoTestClient.doCall(EchoTestClient.java:222) at EchoTestClient.doWork(EchoTestClient.java:147) at EchoTestClient.main(EchoTestClient.java:114) [SOAPException: faultCode=SOAP-ENV:Client; msg=No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/soap/ encoding/'.; targetException=java.lang.IllegalArgumentException: No Deserializer found to deserialize a ':Result' using encoding style 'http://schemas.xmlsoap.org/soap/ encoding/'.] at org.apache.soap.rpc.Call.invoke(Call.java:246) at EchoTestClient.doCall(EchoTestClient.java:222) at EchoTestClient.doWork(EchoTestClient.java:152) at EchoTestClient.main(EchoTestClient.java:114) echoStruct OK echoIntegerArray OK echoFloatArray OK echoStringArray OK echoStructArray OK
The errors occurred for the following reasons:
The generated response did not contain the
xsi:type
information for the return value.
The test client we used looked for an element with the name
<Return>
. The generated response used the
name <Result>
instead.
To compensate for this problem, we added the following code, which
adds another entry in the SOAPMappingRegistry
for
the "Result"
element. This addition is made for
each datatype:
p = new Parameter("inputString", String.class, "Hi there!", null); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Return"), null, null, stringDser); smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "Result"), null, null, stringDser); doCall(url, "echoString", p);
In recognition of this issue, the MS SOAP Toolkit Version 3.0 site
supports two versions of the test: one that expects and returns
xsi:type
information and one that does not. The
test we picked was the untyped version—hence the errors. The
typed version of the test is located at http://mssoapinterop.org/stkV3/InteropTyped.wsdl.
We could have used that version to begin with and declared victory, but we wanted to go through the exercise with you because this issue can arise when communicating between two different kinds of toolkits.
We tried the same test with the Apache Axis version of the Echo Test client and got the following results:
java TestClient URL: http://mssoapinterop.org/stkV3/Interop.wsdl echoString OK echoStringArray OK echoInteger OK echoIntegerArray OK echoFloat OK echoFloatArray OK echoStruct OK echoStructArray OK echoVoid OK echoBase64 OK echoHexBinary OK echoDate OK echoDecimal OK echoBoolean OK echoMap Fail: WSDLReader:None of the matching operations for soapAction http://soapinterop.org/ could successfully load the incoming request. Potential typemapper problem echoMapArray Fail: WSDLReader:None of the matching operations for soapAction http://soapinterop.org/ could successfully load the incoming request. Potential typemapper problem
Many SB Interop participants have built a browser client that remotely executes a test client on your behalf. Figure 9-1 shows the interface to one such site for Iona XMLBus (http://interop.xmlbus.com:7002/InteropTest/index.jsp). You can type in any known WSDL endpoint that participates in the testing and learn how an XMLBus client fared against the remote service.
Figure 9-2 shows an example of the results.