andrewjwelch.com

A SOAP extension for XSLT 2.0

XSLT can do many things, and now it can be a SOAP client too. To demonstrate the extension here's a stylesheet that makes a call to the Amazon webservice looking for books on XSLT:
The XSLT
Here's the XSLT:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:saxon="http://saxon.sf.net/" xmlns:soap="net.sf.kernow.soapextension.SOAPExtension" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" version="2.0" exclude-result-prefixes="xs">

<xsl:output indent="yes" />

<xsl:param name="endpoint" select="'http://soap.amazon.com/onca/soap?Service=AWSECommerceService'" as="xs:string" />

<xsl:variable name="request">
<soapenv:Envelope xmlns:ns="http://webservices.amazon.com/AWSECommerceService/2007-05-14">
   
<soapenv:Body>
      
<ns:ItemSearch>
         
<ns:AWSAccessKeyId>[my awsaccessid - insert your own]</ns:AWSAccessKeyId>
         
<ns:Shared>
            
<ns:Keywords>XSLT</ns:Keywords>
            
<ns:SearchIndex>Books</ns:SearchIndex>
         
</ns:Shared>
      
</ns:ItemSearch>
   
</soapenv:Body>
</soapenv:Envelope>
</xsl:variable>

<xsl:template match="/" name="main">
        
<xsl:apply-templates select="saxon:parse(soap:soapRequest($request, $endpoint))" mode="process-SOAP-message" />
</xsl:template>

<xsl:template match="/" mode="process-SOAP-message">
        
<xsl:copy-of select="." />
</xsl:template>

</xsl:stylesheet>
The actual request here is written directly in the variable $request, but it could easily be pulled in using doc() or created within the stylesheet
The function is in the package net.sf.kernow.soapextension.SOAPExtension and the method is soap:soapRequest($request, $endpoint)). It's included with Kernow so if you run this stylesheet from within Kernow it should just work - otherwise you'll need to ensure the class on the classpath along with Saxon.
The function returns a String which is turned into an XML document by calling saxon:parse(). Calling apply-templates on this will make Saxon look for the root-matching template, so to prevent an endless loop the mode is needed.
The Extension class
Here's the Java:
package net.sf.kernow.soapextension.SOAPExtension;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import net.sf.saxon.om.NodeInfo;

/**
 * Enables the calling of SOAP based web services from XSLT.  
 * @author Andrew Welch
 */
public class SOAPExtension {
    
    public static final String ENCODING = "UTF-8";
    public static final String CONTENT_LENGTH = "Content-Length";
    public static final String CONTENT_TYPE = "Content-Type";
    public static final String CONTENT_TYPE_VALUE = "text/xml; charset=utf-8";    
    public static final String SOAPACTION = "SOAPAction";
    public static final String POST = "POST";
            
    public static String soapRequest(NodeInfo requestXML, String endpoint) {
        String result = makeCall(transformToString(requestXML), endpoint);        
        return result;
    }    
    
    public static String soapRequest(String requestXML, String endpoint) {
        String result = makeCall(requestXML, endpoint);        
        return result;
    }  
    
    private static String transformToString(NodeInfo sourceXML) {

        StringWriter sw = new StringWriter();

        try {
            TransformerFactory tFactory = new net.sf.saxon.TransformerFactoryImpl();
            Transformer transformer = tFactory.newTransformer();
            transformer.transform(sourceXML, new StreamResult(sw));
        } catch (TransformerConfigurationException ex) {
            ex.printStackTrace();
        } catch (TransformerException ex) {
            ex.printStackTrace();
        }
        
        return sw.toString();
    }
    
    private static String makeCall(String requestXML, String endpoint) {
                
        String SOAPUrl = endpoint;
        StringBuilder responseBuf = new StringBuilder();
                
        try {            
            // Create the connection to the endpoint
            URL url = new URL(SOAPUrl);
            URLConnection connection = url.openConnection();
            HttpURLConnection httpConn = (HttpURLConnection) connection;
                                 
            byte[] b = requestXML.getBytes(ENCODING);
            
            // Set the appropriate HTTP parameters.
            httpConn.setRequestProperty(CONTENT_LENGTH, String.valueOf(b.length));
            httpConn.setRequestProperty(CONTENT_TYPE, CONTENT_TYPE_VALUE);
            httpConn.setRequestProperty(SOAPACTION, "");
            
            httpConn.setRequestMethod(POST);
            
            httpConn.setDoOutput(true);
            httpConn.setDoInput(true);
            
            // Send the the request
            OutputStream out = httpConn.getOutputStream();
            out.write(b);
            out.flush();
                       
            // Read the response and write it to the response buffer.            
            InputStreamReader isr = new InputStreamReader(httpConn.getInputStream());
            BufferedReader in = new BufferedReader(isr);
            
            String line;
            while ((line = in.readLine()) != null) {
                responseBuf.append(line);
            } 
            
            in.close();
            
        } catch (ProtocolException ex) {
            ex.printStackTrace();
            return error(ex.getMessage());
        } catch (MalformedURLException ex) {
            ex.printStackTrace();
            return error(ex.getMessage());
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
            return error(ex.getMessage());
        } catch (IOException ex) {
            ex.printStackTrace();
            return error(ex.getMessage());
        }
        
        return responseBuf.toString();
    }
    
    private static String error(String msg) {
        return "" + msg + "";
    }
}
The class accepts requests as either NodoInfo or Strings. If the request is a NodeInfo it's converted to a String using an indentity transform (which you get by calling newTransfomer() without a stylesheet - it's a highly optimized way of serializing XML using Saxon).
An HTTP connection is opened to the endpoint and the request made, and the whole result SOAP message (which is still a String) returned to the transform. The response could be parsed and a NodeInfo returned instead, which would remove the need for saxon:parse() in the transform (but as we're already tightly coupled to Saxon we may as well not rewrite that). It's worth noting that the "SOAPAction" header is required - I originally didn't set this (in Kernow 1.5.0.5 and below) but some web services seem to require it - read here
If any exceptions are thrown, the exception message is returned to the transform wrapped in <error></error> to allow the transform to deal with it.
The Result
Running the above transform in the standalone tab of Kernow gives this result - some classics there:
<SOAP-ENV:Envelope xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 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>
      
<ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2007-05-14">
         
<OperationRequest>
            
<HTTPHeaders>
               
<Header Name="UserAgent" Value="Jakarta Commons-HttpClient/3.0" />
            
</HTTPHeaders>
            
<RequestId>1HXAKAY8C9M9H4P1906T</RequestId>
            
<Arguments>
               
<Argument Name="Service" Value="AWSECommerceService" />
            
</Arguments>
            
<RequestProcessingTime>0.151901006698608</RequestProcessingTime>
         
</OperationRequest>
         
<Items>
            
<Request>
               
<IsValid>True</IsValid>
            
</Request>
            
<TotalResults>85</TotalResults>
            
<TotalPages>9</TotalPages>
            
<Item>
               
<ASIN>0596005792</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596005792%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596005792%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Geoff Coffey</Author>
                  
<Author>Susan Prosser</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>FileMaker Pro 8: The Missing Manual</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0596007647</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596007647%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596007647%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Elliotte Rusty Harold</Author>
                  
<Author>W. Scott Means</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>XML in a Nutshell, Third Edition</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0764569090</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0764569090%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0764569090%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Michael Kay</Author>
                  
<Manufacturer>Wrox</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>XSLT 2.0 Programmer's Reference (Programmer to Programmer)</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0596009747</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596009747%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596009747%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Salvatore Mangano</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>XSLT Cookbook, Second Edition (Cookbooks (O'Reilly))</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0470087889</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0470087889%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0470087889%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Eric van der Vlist</Author>
                  
<Author>Danny Ayers</Author>
                  
<Author>Erik Bruchez</Author>
                  
<Author>Joe Fawcett</Author>
                  
<Author>Alessandro Vernet</Author>
                  
<Manufacturer>Wrox</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>Professional Web 2.0 Programming (Wrox Professional Guides)</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0672323184</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0672323184%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0672323184%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Michiel van Otegem</Author>
                  
<Manufacturer>Sams</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>Sams Teach Yourself XSLT in 21 Days</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>1556220863</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=1556220863%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/1556220863%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Scott Driza</Author>
                  
<Manufacturer>Wordware Publishing, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>Word 2003 Document Automation with VBA, XML, XSLT, and Smart Documents (Wordware Applications Library)</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0596003277</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596003277%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596003277%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Michael Fitzgerald</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>Learning XSLT</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0596000537</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596000537%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596000537%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Doug Tidwell</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>Xslt</Title>
               
</ItemAttributes>
            
</Item>
            
<Item>
               
<ASIN>0596006349</ASIN>
               
<DetailPageURL>http://www.amazon.com/gp/redirect.html%3FASIN=0596006349%26tag=ws%26lcode=sp1%26cID=2025%26ccmID=165953%26location=/o/ASIN/0596006349%253FSubscriptionId=[my awsaccessid - insert your own]</DetailPageURL>
               
<ItemAttributes>
                  
<Author>Priscilla Walmsley</Author>
                  
<Manufacturer>O'Reilly Media, Inc.</Manufacturer>
                  
<ProductGroup>Book</ProductGroup>
                  
<Title>XQuery</Title>
               
</ItemAttributes>
            
</Item>
         
</Items>
      
</ItemSearchResponse>
   
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Making it more useful...
If you want to make several calls using different arguments, you can turn the request variable into a function:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:saxon="http://saxon.sf.net/" xmlns:soap="net.sf.kernow.soapextension.SOAPExtension" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:func="func" version="2.0" exclude-result-prefixes="xs">

<xsl:output indent="yes" />

<xsl:param name="endpoint" select="'http://soap.amazon.com/onca/soap?Service=AWSECommerceService'" as="xs:string" />

<xsl:function name="func:request" as="element()">
        
<xsl:param name="keywords" as="xs:string+" />
        
<xsl:param name="searchIndex" as="xs:string" />

        
<soapenv:Envelope xmlns:ns="http://webservices.amazon.com/AWSECommerceService/2007-05-14">
           
<soapenv:Body>
              
<ns:ItemSearch>
                 
<ns:AWSAccessKeyId>[my awsaccessid - insert your own]</ns:AWSAccessKeyId>
                 
<ns:Shared>
                    
<ns:Keywords><xsl:value-of select="$keywords" /></ns:Keywords>
                    
<ns:SearchIndex><xsl:value-of select="$searchIndex" /></ns:SearchIndex>
                 
</ns:Shared>
              
</ns:ItemSearch>
           
</soapenv:Body>
        
</soapenv:Envelope>

</xsl:function>

<xsl:template match="/" name="main">
        
<xsl:variable name="request" select="func:request('XSLT', 'Books')" as="element()" />
        
<xsl:apply-templates select="saxon:parse(soap:soapRequest($request , $endpoint))" mode="process-SOAP-message" />
</xsl:template>

<xsl:template match="/" mode="process-SOAP-message">
        
<xsl:copy-of select="." />
</xsl:template>

</xsl:stylesheet>
And that's it, a nice easy way to access SOAP based web services in XSLT.




top