Glen Mazza's Weblog

« Creating integration... | Main | TightBlog 3.0 Releas... »

https://glenmazza.net/blog/date/20180506 Sunday May 06, 2018

Using SAAJ to call RPC/encoded SOAP web services

The National Weather Service's legacy National Digital Forecast Service (WSDL) uses the older rpc/encoded SOAP binding style not ordinarily supported by JAX-WS implementations (the NWS has since switched to a REST-based API). The WS-I Basic Profile limits binding styles to either Document/literal or RPC/literal, and JAX-WS was designed to honor this limitation. The reason for excluding RPC/encoded was apparently due to compatibility issues involved with encoding, as well as possibly message size and performance issues.

Russell Butek has written an informative article explaining the different SOAP binding styles, their appearance over the wire, and the advantages and disadvantages of each. To show the binding differences between the RPC/encoded and standard Doc/Literal bindings, I've copied an NWS and an Amazon Commerce Service (WSDL) operation below:

<binding name="ndfdXMLBinding" type="tns:ndfdXMLPortType">
   <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
   <operation name="NDFDgen">
      <soap:operation 
         soapAction="http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl#NDFDgen" 
         style="rpc"/>
      <input>
         <soap:body use="encoded" 
            namespace="http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl" 
            encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
      </input>
      <output>
         <soap:body use="encoded" 
            namespace="http://www.weather.gov/forecasts/xml/DWMLgen/wsdl/ndfdXML.wsdl"
            encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
      </output>
   </operation>
   ...
</binding>

<binding name="AWSECommerceServiceBinding" type="tns:AWSECommerceServicePortType">
   <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
   <operation name="ItemSearch">
      <soap:operation soapAction="http://soap.amazon.com"/>
      <input>
         <soap:body use="literal"/>
      </input>
      <output>
         <soap:body use="literal"/>
      </output>
   </operation>
   ...
</binding>

In addition to requiring Doc/Lit or RPC/Lit bindings, the WS-I Basic Profile also prohibits the use of the encodingStyle attribute (R1005-R1007) and namespace attributes (R2716-17; R2726), restrictions you can see honored above with the Amazon ItemSearch operation.

Web service implementations that natively support JAX-RPC service calls include Oracle's JAX-RPC implementation as well as Axis 1.x, both long deprecated. Attempting to run Apache CXF's wsdl2java with the NWS' RPC/encoded WSDL returns an "Rpc/encoded wsdls are not supported in JAXWS 2.0" error message. However the SOAP with Attachments API for Java (SAAJ) can be used with the JAX-WS Dispatch interface to create "raw" web service calls to a web service provider that uses RPC/encoded bindings. The following SOAP client provides two such examples of using SAAJ. A simple way to run this example would be to download my intro Web Service tutorial source code, replace its client subproject's WSClient class with the WSClient below and run mvn clean install exec:exec from the client folder.

package client;

import java.io.StringReader;
import java.net.URL;

import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPFactory;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.soap.SOAPFaultException;

public class WSClient {

    public static void main (String[] args) {
        WSClient wsc = new WSClient();

        // get forecast by Zip Code
        wsc.getWeatherForecast("19110"); // Philadelphia

        // get another forecast
        wsc.getWeatherForecast("33157"); // Miami
    }

    private void getWeatherForecast(String zipCode) {

        try {
            // Convert the ZIP code to a geocoded value (which is needed
            // as input for the weather data)

            String nsSchema = "http://graphical.weather.gov/xml/DWMLgen/schema/DWML.xsd";

            String soapSchema = "http://schemas.xmlsoap.org/soap/envelope/";

            String xsiSchema
                    = "http://www.w3.org/2001/XMLSchema-instance";

            String encodingStyle
                    = "http://schemas.xmlsoap.org/soap/encoding/";

            String zipRequest = " "
                    + ""
                    +"   "
                    +           ""
                    +               zipCode
                    +           ""
                    +    ""
                    +""
                    +"";

            String wsdl = "https://graphical.weather.gov/xml/SOAP_server/ndfdXMLserver.php?wsdl";
            String targetNS = "https://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl";

            URL url = new URL(wsdl);
            QName serviceName = new QName(targetNS, "ndfdXML");
            QName portName = new QName(targetNS, "ndfdXMLPort");
            Service service = Service.create(url, serviceName);

            /*
             * JAX-WS Dispatch provides three usage options: -- JAXBContext
             * (unsure if works though for rpc/enc WSDL) -- JAXP Source objects
             * (used here) -- SAAJ SOAPMessages (used in 2nd request below)
             */
            Dispatch dispatch = service.createDispatch(portName,
                    Source.class, Service.Mode.MESSAGE);
            Source zipResponse = dispatch.invoke(
                    new StreamSource(new StringReader(zipRequest)));
            // if using a file for input instead:
            // new StreamSource(new File("myrequest.xml")));

            // use SAAJ to open message -- check if error or valid data
            MessageFactory msgFactory = MessageFactory.newInstance();
            SOAPMessage geocodeMsg = msgFactory.createMessage();
            SOAPPart env = geocodeMsg.getSOAPPart();
            env.setContent(zipResponse);
            // writeTo method outputs SOAPMessage, helpful for debugging
            // geocodeMsg.writeTo(System.out);

            if (geocodeMsg.getSOAPBody().hasFault()) {
                // Copy official error response into our LNF Fault
                SOAPFault fault = geocodeMsg.getSOAPBody().getFault();
                System.out.println("Could not obtain forecast for zipcode "
                        + zipCode + ": "
                        + fault.getFaultString() + "; " + fault.getDetail().getValue());
            }

            // From here: valid geocode is present-- so get weather report next

            /*
             * LatLonListZipCodeResponse is not very helpful; needed information
             * (latLonList) element is html-escaped instead of a real tag, which
             * is suitable for HTML responses but not so helpful when you need
             * to extract the value. So will need to parse string response to
             * get geocode values  
             *      35.1056,-90.007
             *   
             *  
             * 
             */
            String geocodeBuffer = geocodeMsg.getSOAPBody().
                    getElementsByTagName("listLatLonOut")
                    .item(0).getFirstChild().getNodeValue();

            // .getNodeValue() unescapes HTML string
            String geocodeVals = geocodeBuffer.substring(
                    geocodeBuffer.indexOf("") + 12,
                    geocodeBuffer.indexOf(""));
            System.out.println("Geocode Vals for zip code " + zipCode
                    + " are: " + geocodeVals);

            /*
             * NDFDgenLatLonList operation: gets weather data for a given
             * latitude, longitude pair
             *
             * Format of the Message:     38.99,-77.02 
             *  glance
             *    
             */
            SOAPFactory soapFactory = SOAPFactory.newInstance();
            SOAPMessage getWeatherMsg = msgFactory.createMessage();
            SOAPHeader header = getWeatherMsg.getSOAPHeader();
            header.detachNode();  // no header needed
            SOAPBody body = getWeatherMsg.getSOAPBody();
            Name functionCall = soapFactory.createName(
                    "NDFDgenLatLonList", "schNS",
                    nsSchema);
            SOAPBodyElement fcElement = body.addBodyElement(functionCall);
            Name attname = soapFactory.createName("encodingStyle", "S",
                    soapSchema);
            fcElement.addAttribute(attname, soapSchema);
            SOAPElement geocodeElement = fcElement.addChildElement("listLatLon");
            geocodeElement.addTextNode(geocodeVals);
            SOAPElement product = fcElement.addChildElement("product");
            product.addTextNode("glance");

            // make web service call using this SOAPMessage
            Dispatch smDispatch = service.createDispatch(portName,
                    SOAPMessage.class, Service.Mode.MESSAGE);
            SOAPMessage weatherMsg = smDispatch.invoke(getWeatherMsg);
            // weatherMsg.writeTo(System.out); // debugging only

            // Metro needs normalize() command because it breaks
            // up child dwml element into numerous text nodes.
            weatherMsg.getSOAPBody().getElementsByTagName("dwmlOut")
                    .item(0).normalize();

            // First child of dwmlOut is the dwml element that we need.
            // It is the root node of the weather data that we will
            // be using to generate the report.
            String weatherResponse = weatherMsg.getSOAPBody().
                    getElementsByTagName("dwmlOut")
                    .item(0).getFirstChild().getNodeValue();
            System.out.println("WR: " + weatherResponse);
        } catch (SOAPFaultException e) {
            System.out.println("SOAPFaultException: " + e.getFault().getFaultString());
        } catch (Exception e) {
            System.out.println("Exception: " + e.getMessage());
        }
    }
}

In the client code above I used the rather nonintuitive DOM Tree API to get the data elements I needed, for example:

String weatherResponse = weatherMsg.getSOAPBody().getElementsByTagName("dwmlOut")
    .item(0).getFirstChild().getNodeValue();

If you have many such calls to make, another option is to use XPath, see tutorials from Baeldung and TutorialsPoint for more information.

Comments

Post a Comment: