Max is a software development consultant specializing in C++, .NET development, and code optimization. He is also the author of Enterprise Application Development with Visual C++ .NET 2005 (Charles River Media, 2006). Max can be contacted at [email protected].
Interoperability is one of many challenges that developers of distributed network applications face. Software interoperability becomes especially important when different parts of an application are developed using different tools, or when the entire application (or its components) must interface with third-party software. In theory, interoperability is achieved by adhering to industry standards, such as XML, WSDL, and SOAP, at least as far as web services are concerned. In practice, however, standards are often subject to interpretation and divergent implementations that are not fully compatible. In this article, I use Oracle JDeveloper 10.1.2 to examine the problems you must overcome when consuming ATL/.NET web services in Java clients that rely on Apache SOAP.
Creating a Web-Service Proxy
The first step in consuming web services in Java using Oracle JDeveloper is to create a web-service proxy. This is done by creating an empty Application Workspace with an empty project and then adding a Web Service Proxy (located under Business Tier, Web Services) from the New Gallery dialog; see Figure 1. Then you enter "Web Service WSDL URL" and finish the dialog. At this point, Oracle's code-generation wizard generates the source code necessary for consuming the specified web service. If the web service you just consumed is simple, dealing only with primitive types and no SOAP headers, additional effort is not necessary. However, if the web service you are trying to consume is complex and written using .NET or ATL, this is just the beginning of a number of manual chores that you must do to consume the service.
Although JDeveloper-generated web-service stub code implements all web-service methods, you may experience difficulties:
- SOAP headers are not supported by default.
- SOAP fault detail text is not properly reported.
- Output parameters are missing from web-method stubs.
- User-defined type (UDT) fields are expected to obey camel capitalization convention (lowercase, with the first letter of words and all letters of acronyms capitalized) regardless of their actual capitalization.
- Web methods with return values or output parameters fail due to type-mapping errors.
- Web methods returning null values or zero-size arrays (xsi:nil) fail due to type-mapping errors.
The reason for the last two issues is because SOAP response messages generated by .NET and ATL Server web services generally lack embedded XSD-type information in output elements. However, Apache SOAP implementation used by Oracle requires type information for every parameter or UDT field in the response message. A similar issue is with zero-size arrays: Apache SOAP expects a zero-dimension array (for example, <return soapenc:arrayType="MyArrayType[0]" />) when the array contains no elements. However, by default .NET and ATL Server web services generate a nonarray-type element with an xsi:nil="1" attribute. Fortunately, all of these issues can be overcome with some modifications (albeit extensive) to the generated code and a bit of hacking of the Apache SOAP implementation; for example, see the UltraMax ATL Server web service and corresponding Java web-service proxy code (available at http://www.ddj.com/ftp/2006/200604/).
Implementing Web Method Call Templates
The first thing you need to do is implement a reusable web method call template to replace wizard-generated code. The template must:
- Support accurate type mapping even when XSD type information is missing from the SOAP response message.
- Support SOAP headers as needed.
- Report SOAP faults as exceptions, providing detailed error-message text.
Listing One is an example of the InvokeCall template and related helper methods. The InvokeCall method relies on InitializeCall to specify the web method name and prepare the SOAP header with the makeHeader method. The code in the makeHeader method manually constructs SOAP header documents to be transmitted with the web method request. In Listing One, the SOAP header corresponds to this XML structure:
<m_Header>
<m_SessionID></m_SessionID>
</m_Header>
The ReportFault method parses the <SOAP:Fault> section of the SOAP response by examining the <detail> element. The trick here is that if the <detail> element does not contain any child nodes but contains directly embedded detail text, getDetailEntries() returns null. Thus you have to add a dummy child node to <detail> (for example, <info> in the example) to encapsulate the detail message text. This has to be done on the ATL Server web-service side in the Error method (see UltraMaxService.cpp, available at http://www.ddj.com/ftp/2006/200604/). Fortunately, the addition of the dummy <info> child node does not affect .NET clients consuming the ATL Server web service because SoapException.Detail.innerText produces clear text with all child tags removed (which is not, however, the case for legacy VB6 clients consuming the service via the SOAP Toolkit; these would have to strip the <detail> and <info> tags manually).
Using the InvokeCall method in the LogOn web method results in:
public void LogOn(String login,
String password) throws Exception
{
Vector params = new Vector();
params.addElement(new Parameter
("login", String.class, login, null));
params.addElement
(new Parameter("password",
String.class, password,null));
Response response =
InvokeCall("LogOn", params);
// Extract SessionID from SOAP header
Header header = response.getHeader();
Vector headerEntires = header.getHeaderEntries();
XMLElement headerElement =
(XMLElement)headerEntires.firstElement();
XMLElement node =
(XMLElement)headerElement.
selectSingleNode("m_SessionID");
// Set SessionID
setSessionId(node.getText());
}
The method contains code for extracting the returned value of the m_SessionID from the SOAP header in the response message. Manual actions such as this are necessary for all web-method calls that provide return values in SOAP headers in SOAP response messages. In this example, this happens only in the LogOn method (session ID is returned). The SOAP header is automatically transmitted with each web method thanks to the InvokeCall template.
Type Mapping and User-Defined Types
Apache SOAP relies on the BeanSerializer class for serializing UDTs and on specialized serializers for serialization of string, float, and other primitive types. Therefore, it is useful to define the ResetBeanSerializer method, which registers in the SOAPMappingRegistry all UDTs with BeanSerializer. For example:
private void ResetBeanSerializer()
{
// Associate bean serializer
// with all user-defined types
m_smr = new SOAPMappingRegistry();
m_smr.mapTypes(Constants.NS_URI_SOAP_ENC,
new QName("urn:UltraMaxService",
"CUltraMaxUser"),
CUltraMaxUser.class, m_beanSer, m_beanSer);
}
This example has only one UDT (CUltraMaxUser), but you need to map all of your UDTs this way if you have more than one. Notice that the ResetBeanSerializer method appears in the web-service stub constructor as well as in the InvokeCall template. In the latter, seemingly redundant placing is necessary for clearing SOAPMappingRegistry from any custom type mapping performed for UDTs appearing in output parameters. Listing Two contains code for mapping UDTs and two helper functionsInvokeUdtTemplate and InvokeUdtArrayTemplatewhich simplify implementation of web methods, returning a UDT or an array of UDTs.
When SOAP response messages contain a <return> element that lacks schema information, Apache SOAP fails to deserialize it even though the schema information could have been specified in the web-service WSDL. The workaround is to explicitly instruct SOAPMappingRegistry as to which deserializer to use to deserialize the return element:
m_smr.mapTypes(Constants.NS_URI_SOAP_ENC,
new QName("", "return"), cls, null, deser)
An apparent bug in the Apache SOAP implementation requires that the mapTypes method is called twice when UDTs are mappedonce for empty namespace and again for the default web-service namespace.
Another necessary step is to explicitly specify the deserializer for each field of the UDT. This is achieved with the help of the MapType method, which relies on Java Reflection to query UDT properties and pick a deserializer based on the property type, with UDT properties being mapped recursively.
Just as arrays are deserialized using the ArraySerializer, the array element is also mapped using the MapType method (see InvokeUdtArrayTemplate in Listing Two).
With these template implementations of web methods, returning a UDT or an array of UDTs is straightforward:
public CUltraMaxUser GetUser()
throws Exception
{
return (CUltraMaxUser)
InvokeUdtTemplate("GetUser",
CUltraMaxUser.class, null);
}
public CUltraMaxUser[] GetUsers()
throws Exception
{
return (CUltraMaxUser[])
InvokeUdtArrayTemplate("GetUsers",
CUltraMaxUser[].class, null);
}
Unfortunately, more work is needed to overcome another bug in the Apache SOAP implementation. By default, the BeanSerializer class assumes and expects each UDT field to adhere to camel-naming conventions; for example, a field must be named firstName rather than FirstName. Unavoidably, such totally unwarranted expectations cause deserialization errors if fields of UDTs do not follow camel-capitalization conventions. To solve this, you must implement these modifications:
- Hack the BeanSerializer source (http://ws.apache.org/soap/) by replacing the case-sensitive field comparison with a case insensitive one, renaming the BeanSerializer class as MyBeanSerializer, and using MyBeanSerializer in place of the BeanSerializer (see the code in the WebServiceClient JDeveloper project; available at http://www.ddj.com/ftp/2006/200604/).
- Provide a custom SimpleBeanInfo-derived class for each UDT that does not adhere to the camel-capitalization convention for its properties. (Java Reflection forcibly changes capitalization of noncamel fields and makes them camel like, which causes BeanSerializer to fail finding the actual field in the UDT.)
Listing Three is a custom bean information class for the CUltraMaxUser UDT. A PropertyDescriptor is created for each UDT field with the capitalization of the field name properly preserved.
Handling null Return Values and Zero-Size Arrays
Once type mapping is implemented, the only other hurdle when consuming ATL web services involves null values and zero-size arrays. The ATL Server SOAP handler generates an xsi:nil="1" attribute when the element's value is null, or when the array is null or zero-size. Unfortunately, Apache SOAP fails to deserialize UDTs whose fields contain the xsi:nil="1" attribute and expects zero-size arrays to be represented as XSD arrays with the dimension parameter set to zero (for example, soapenc:arrayType=mytype[0]).
There are two workarounds to this problem. The simplest one is to catch a SOAP exception thrown by the InvokeCall method and see if the FaultCode equals to "SOAP-ENV:Client"; see the InvokeCallEmpty method in Listing Two. The underlying assumption is that a SOAP exception is thrown on the client side only when the expected value is null. However, if a SOAP exception on the client side can be thrown for other reasons, or if you are invoking a web method that returns multiple output array parameters, this exception-catching approach will not work. Why? Because it deprives additional output parameters from their values when a zero-size array is encountered.
An alternative solution is to modify the ATL library by commenting out lines 4754 through 4760 in atlsoap.h (assuming Visual Studio .NET 2003). This tells the GetArrayInformation method to abort when the array dimension is zero. You would then have to recompile the ATL library by executing this command at the command prompt from the vc7\atlmfc\src directory:
nmake /f atlmfc.mak ATL
You must register Visual C++ environment variables first by executing vcvars.bat (located in vc7\bin folder) at the command prompt, then running nmake, and finally copying the generated library files from the newly created vc7\atlmfc\lib\INTEL folder to the parent lib directory.
The last trick is to allocate a zero-size array using GetMemMgr()->Allocate, instead of setting the array pointer to null. Setting it to null would result in an xsi:nil="1" attribute being generated rather than in zero-size array. For example:
HRESULT CUltraMaxService::GetUsers
(LONG* size, CUltraMaxUser** users)
{
PROLOG()
*size = 0;
*users = (CUltraMaxUser*)
GetMemMgr()->Allocate(0); // IMPORTANT!
EPILOG()
}
Treating Output Parameters
Until now, I have dealt with web methods that return a single value that, as far as Apache SOAP is concerned, are equivalent to web methods with a single output parameter. However, web methods that produce multiple output parameters require special treatment.
For one thing, Java does not support output parameters. Consequently, if a web method requires multiple output parameters, a UDT must be defined on the client side to host the output values. Also, the output parameter value must be extracted manually from the output parameter array and from the return value itself (the first output parameter is always placed in the return value by Apache SOAP); see the GetTrialPeriodSpan method in Listing Four.
Conclusion
Although consumption of reasonably complex ATL/.NET web services in Java using Apache SOAP is a chore, the process can be streamlined using the template helper methods I've presented here.
DDJ
private Response InvokeCall(String methodName, Vector params) throws Exception { try { Call call = InitializeCall(methodName, params); // Invoke call Response response = call.invoke(m_endpointURL,"#" + call.getMethodName()); // Report server-side SOAP fault (if any) if ( response.generatedFault() ) ReportFault(response); return response; } catch(Exception e) { throw e; } finally { // Reset SOAP type mappings to default state erasing all custom // mappings done for each specific SOAP method call ResetBeanSerializer(); } } private Call InitializeCall(String methodName, Vector params) throws ParserConfigurationException { Call call = new Call(); call.setSOAPTransport(m_httpConnection); call.setTargetObjectURI("urn:UltraMaxService"); call.setMethodName(methodName); call.setEncodingStyleURI(Constants.NS_URI_SOAP_ENC); call.setParams(params); // SOAP header support call.setHeader(makeHeader()); // User-define type mapping support call.setSOAPMappingRegistry(m_smr); return call; } private void ResetBeanSerializer() { // Associate bean serializer with all user-defined types m_smr = new SOAPMappingRegistry(); m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("urn:UltraMaxService", "CUltraMaxUser"), CUltraMaxUser.class, m_beanSer, m_beanSer); } // Builds SOAPHeader containing SessionID public Header makeHeader() throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.newDocument(); Header header = new Header(); Vector headerEntries = new Vector(); // Build our sessionId element via DOM and add it to the header Element headerElement = doc.createElement("m_Header"); Element sessionIdElement = doc.createElement("m_SessionID"); sessionIdElement.setAttribute("xsi:type", "xsd:string"); sessionIdElement.appendChild(doc.createTextNode(getSessionId())); headerElement.appendChild(sessionIdElement); headerEntries.add(headerElement); header.setHeaderEntries(headerEntries); return header; } // Throws SOAPException containing detail information public void ReportFault(Response response) throws SOAPException { String detailText = null; Fault fault = response.getFault(); Vector detailEntires = fault.getDetailEntries(); if ( detailEntires.size() > 0 ) { XMLElement detail = (XMLElement)detailEntires.get(0); detailText = detail.getText(); } else detailText = fault.getFaultString(); throw new SOAPException(fault.getFaultCode(), detailText); }Back to article
Listing Two
public void MapType(Class c) throws Exception { // Make sure array item type is mapped if ( c.isArray() ) c = c.getComponentType(); // Map only UDTs defined in this package String className = c.getName(); if ( className.indexOf(CLASS_PREFIX) != 0 ) return; Method[] methods = c.getMethods(); for ( int i = 0; i < methods.length; i++ ) { String methodName = methods[i].getName(); if ( methodName.indexOf("get") == 0 ) { String propertyName = methodName.substring(3); Class propertyType = methods[i].getReturnType(); String propertyTypeName = propertyType.getName(); if ( propertyTypeName.compareTo("java.lang.String") == 0 ) m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), null, null, m_stringSer); else if ( propertyTypeName.compareTo("java.util.Date") == 0 ) m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), null, null, new DateSerializer()); else if ( propertyTypeName.compareTo("boolean") == 0 ) m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), null, null, m_boolSer); else if ( propertyTypeName.compareTo("java.lang.Integer") == 0 || propertyTypeName.compareTo("int") == 0 ) m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), null, null, m_intSer); else if ( propertyTypeName.compareTo("java.lang.Float") == 0 || propertyTypeName.compareTo("float") == 0 ) m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), null, null, m_floatSer); else if ( propertyTypeName.indexOf(CLASS_PREFIX) == 0 ) { String truncatedName = propertyTypeName.substring(CLASS_PREFIX.length()); m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", propertyName), propertyType, m_beanSer, m_beanSer); m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("urn:UltraMaxService", truncatedName), propertyType, m_beanSer, m_beanSer); MapType(propertyType); } } } } // Invokes SOAP method and returns null when response is empty but // non-empty response is expected private Response InvokeCallEmpty(String methodName, Vector params) throws Exception { Response response = null; try { // Throws SOAPException when the <return> element is empty response = InvokeCall(methodName, params); } catch (SOAPException ex) { String faultCode = ex.getFaultCode(); // If the SOAPException is generated on client side it is likely // to be due to deserialization error, most probably empty array if ( faultCode.compareTo("SOAP-ENV:Client") == 0 ) return null; else throw ex; } return response; } // Invokes SOAP method and returns an array of UDTs public Object InvokeUdtArrayTemplate(String methodName, Class cls, Vector params) throws Exception { // Return type UDT[] m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "return"), cls, m_arraySer, m_arraySer); // Mapp array element UDT MapType(cls); Response response = InvokeCallEmpty(methodName, params); // Return value if ( response != null ) { Parameter result = response.getReturnValue(); return result.getValue(); } else return null; } // Invokes SOAP method and returns an UDT public Object InvokeUdtTemplate(String methodName, Class cls, Vector params) throws Exception { Deserializer deser = null; if ( cls == String.class ) deser = m_stringSer; else if ( cls == Integer.class ) deser = m_intSer; else if ( cls == Boolean.class ) deser = m_boolSer; else if ( cls == Float.class ) deser = m_floatSer; else deser = m_beanSer; m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "return"), cls, null, deser); m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("urn:UltraMaxService", "return"), cls, null, deser); MapType(cls); Response response = InvokeCall(methodName, params); // Return value Parameter result = response.getReturnValue(); return result.getValue(); }Back to article
Listing Three
public class CUltraMaxUserBeanInfo extends SimpleBeanInfo { private final static Class target = CUltraMaxUser.class; public PropertyDescriptor[] getPropertyDescriptors() { try { return new PropertyDescriptor[] { new PropertyDescriptor("FirstName", target.getMethod("getFirstName", null), target.getMethod("setFirstName", new Class[] {String.class})), new PropertyDescriptor("LastName", target.getMethod("getLastName", null), target.getMethod("setLastName", new Class[] {String.class})), }; } catch (IntrospectionException e1) { throw new Error(e1.toString()); } catch (NoSuchMethodException e2) { throw new Error(e2.toString()); } }Back to article
public TrialPeriodSpan GetTrialPeriodSpan() throws Exception { // Output parameter String m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "start"), String.class, null, m_stringSer); // Return type Boolean m_smr.mapTypes(Constants.NS_URI_SOAP_ENC, new QName("", "end"), String.class, null, m_stringSer); Response response = InvokeCall("GetTrialPeriodSpan", null); TrialPeriodSpan span = new TrialPeriodSpan(); // Output parameters Vector parameters = response.getParams(); for ( int i = 0; i < parameters.size(); i++ ) { Parameter outParameter = (Parameter)parameters.get(i); String parameterName = outParameter.getName(); Object parameterValue = outParameter.getValue(); if ( parameterName.compareTo("start") == 0 ) span.setStart((String)parameterValue); else if ( parameterName.compareTo("end") == 0 ) span.setEnd((String)parameterValue); } // Return value Parameter returnValue = response.getReturnValue(); String returnValueName = returnValue.getName(); String returnValueValue = (String)returnValue.getValue(); if ( returnValueName.compareTo("start") == 0 ) span.setStart(returnValueValue); else if ( returnValueName.compareTo("end") == 0 ) span.setEnd(returnValueValue); return span; }Back to article