XSLT Web Service Clients
December 1, 2004
I love the free picture-sharing website flickr. I love how I can upload pictures and add as much or as little descriptive metadata as I wish. I can point anyone to these pictures, and they can just view them with no need to register. (For example, see these alien head prizes in Cape May, New Jersey, waiting for the lucky children who accumulate enough tickets to win them at the little boardwalk's arcade.) I can even add tags to a web page to insert thumbnails of randomly selected pictures from my flickr collection into that web page, complete with a link to the full size version, as I did recently to my home page.
I also love how they offer REST, XML-RPC, and SOAP APIs to their data. This gives me more motivation to enter metadata for my pictures because I know that I can download the metadata as XML and store it in case some change in flickr's business plan renders my metadata inaccessible. (These things happen.)
Recently, I was writing a Python script to pull down my flickr metadata and planning the XSLT stylesheet that would process the results. The stylesheet would take the lists of photo sets and pictures generated by the various calls to the flickr API and combine the results into a single cross-referenced file.
Then I realized that I didn't need Python at all for this. Popular XML parsers can read remote resources, and the reason we call them "resources" and not "files" is that they can be more than just files; they can be anything retrieved by a GET request to an HTTP server—like the result of a call to a web service!
A few months ago in this column, I wrote about how a RESTful URL can send a query to Amazon that requests the results in XML. The same URL can identify an XSLT stylesheet to run against the results of the query. The interesting part of the Amazon API's approach is that the stylesheet is run on the server side, and the result of the transformation is returned by Amazon to you.
Running a stylesheet on the client side after pulling down source tree data from a remote source seemed so simple and obvious that I didn't consider it worth writing about—until I considered the implications of pulling down the results of a web service call and using them to process some other data.
Currency Conversion for an Expense Report
For example, let's take a classic web services use case: currency conversion. I want to create a stylesheet that copies the following XML document, adding an additional amount child element with the U.S. dollar value of each purchase.
<expenses> <item date="2004-11-03"> <desc>car rental</desc> <location>Amsterdam, Holland</location> <amount currencyCode="EUR">183.45</amount> </item> <item date="2004-11-04"> <desc>dinner</desc> <location>London, England</location> <amount currencyCode="GBP">35</amount> </item> <item date="2004-11-06"> <desc>hockey stick</desc> <location>Montreal, Canada</location> <amount currencyCode="CAD">52.00</amount> </item> </expenses>
Each amount element's currencyCode attribute has the ISO 4217 code to indicate the currency for the amount stored in the element's content. The second amount element that the stylesheet will add to each item element will have a currencyCode value of "USD" to show that the new amount element stores the U.S. dollar equivalent of the value from the original amount element.
The site webservicex.net provides a Currency Convertor web service that includes a RESTful HTTP GET interface. (Clicking around their website, I couldn't find a complete list of their RESTful web services, but a Google query of their site provides an extensive list.) For example, the URL http://www.webservicex.net/CurrencyConvertor.asmx/ConversionRate?FromCurrency=EUR&ToCurrency=GBP returns the current multiplier to convert euros to British pounds. (Note the FromCurrency and ToCurrency parameters in the URL.) After sending your browser to this URL, you'll only see a number displayed, but a View Source will show the XML that was returned. It will resemble this, depending on the exchange rate the day you try it:
<?xml version="1.0" encoding="utf-8"?> <double xmlns="http://www.webserviceX.NET/">0.6943</double>
We'll want the value but not its enclosing element.
The following stylesheet copies the expenses document shown above, calling the ConversionRate web service to find the right conversion multiplier and using this value to calculate the U.S. dollar equivalent of each item element's amount. To make the call to the web service a bit more readable, the first template rule, which is responsible for processing each item element, creates two variables. The first, "conversionURL", stores the URL used to call the web service. The second variable, "convMultiplier", calls the web service, pulls the double element value out of the little document that gets retrieved, and converts it to a number so that it can be used in a mathematical expression later in the stylesheet.
Note in the example of the retrieved conversion rate shown above that the double element provided by the web service is in the http://www.webserviceX.NET namespace. Because of this, the stylesheet declares that namespace with a prefix of "ws" and then uses that prefix to identify the element with the value it needs: ws:double. (Note also the exclude-result-prefixes attribute of the xsl:stylesheet element, which ensures that the associated namespace node doesn't get copied to the result document.)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ws="http://www.webserviceX.NET/" exclude-result-prefixes="ws" version="1.0"> <xsl:template match="item"> <xsl:variable name="conversionURL"> <xsl:text>http://www.webservicex.net/CurrencyConvertor.asmx/ ConversionRate?FromCurrency=</xsl:text> <xsl:value-of select="amount/@currencyCode"/> <xsl:text>&ToCurrency=USD</xsl:text> </xsl:variable> <xsl:variable name="convMultiplier"> <xsl:value-of select="number(document($conversionURL)/ws:double)"/> </xsl:variable> <xsl:copy> <xsl:apply-templates/> <amount currencyCode="USD"> <xsl:value-of select="$convMultiplier * amount"/> </amount> </xsl:copy> </xsl:template> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> </xsl:copy> </xsl:template> </xsl:stylesheet>
The document() function makes the call to the web service possible. The March, 2002 "Transforming XML" column Reading Multiple Input Documents showed how this function can combine the results of multiple documents into a single result document. The article's examples used the function to read in local documents, but with a full URL and an internet connection, you can pull in any resource you can name, including the results of a call to a web service. While the stylesheet above makes the call in an xsl:value-of element, the "Reading Multiple Input Documents" column showed how it could also be called from an xsl:apply-templates element, which tells the XSLT processor to apply any relevant templates in the stylesheet to the retrieved XML. We'll see an example of this below.
After copying the existing item element from the source tree, but before copying its end tag across, the stylesheet's first template rule adds the new amount element with a hard-coded value of "USD" in its currencyCode attribute. The value of this new amount element is calculated using the multiplier from the convMultiplier variable. The stylesheet's other template rule just copies the source document's remaining elements and attributes to the result tree.
When I run this stylesheet against the document shown earlier, I get this result (new elements contained in bold tags):
<?xml version="1.0" encoding="utf-8"?><expenses> <item> <desc>car rental</desc> <location>Amsterdam, Holland</location> <amount currencyCode="EUR">183.45</amount> <b><amount currencyCode="USD">236.558775</amount></b></item> <item> <desc>dinner</desc> <location>London, England</location> <amount currencyCode="GBP">35</amount> <b><amount currencyCode="USD">64.9635</amount></b></item> <item> <desc>hockey stick</desc> <location>Montreal, Canada</location> <amount currencyCode="CAD">52.00</amount> <b><amount currencyCode="USD">43.342</amount></b></item> </expenses>
Can we round off the calculated values to not display partial cents? Floating point math is outside of the scope of XSLT 1.0, so you won't find consistent behavior from one processor to another. Leave that to the program that ultimately formats the document above for display. Or, if you're using an XSLT 2.0 processor, you can use XPath 2.0's new fn:round function.
Using More Complex XML
The XML returned by the ConversionRate web service was a single element with a little PCDATA and no attributes other than its namespace declaration. It couldn't have been much simpler. Most web services return more complex XML, which reminds us why XML is such a good fit with web services: because it makes it easy to send chunks of data with arbitrary yet clear structures. Your XSLT stylesheet that pulls these chunks of XML data from a web service may want to reach inside them and pull specific values out. Your stylesheet may even want to make further web service calls based on what it finds there. I've put the stylesheet I use to download my flickr.com metadata at this URL. (The "api_key" variable is set to a dummy value; you'll have to get your own before you can actually run the stylesheet.)
This stylesheet doesn't care about any source document because it will get all the data it needs from calls to the flickr API. It's tempting to say "use any well-formed document you like as the source document" and then trigger the main template rule upon finding a match pattern of "/". This will cause problems, though, because each time the XSLT processor grabs a document using the document() function, it applies all relevant template rules to that document. That document has its own root, so a template rule with a match pattern of "/" gets applied to that document as well, and so my first draft of this stylesheet was a recursive mess. I changed it to look for the element "xsl:stylesheet" and then used the stylesheet itself (or any XSLT stylesheet) as the "source" document.
The stylesheet calls four different methods of the flickr API. The first, flickr.photosets.getList, gets a list of the groupings I've made of my flickr photos. (As I write this, I only have three: "hits," "Albemarle County Fair, 2004," and "Thanksgiving 2004.")
The same template rule also retrieves a list of all the photos. The flickr.photos.search method queries for pictures meeting a certain condition, and with my user ID sent as the only parameter, it returns a list of all of my pictures.
Note that the second method call isn't necessarily being invoked right after the first. The first one, flickr.photosets.getList, pulls down some XML, which gets processed by the stylesheet according to any applicable template rules in that stylesheet. The second template rule processes photoset elements returned by flickr.photosets.getList, making a flickr.photosets.getPhotos API call for each photoset element it finds. Similarly, the third template rule calls the flickr.photos.getInfo method for each photo element returned by the flickr.photos.search method called from the first template rule. The combination of these three template rules demonstrate how dynamic an XSLT web client can be because most of this stylesheet's API calls are driven by the results of earlier calls—it pulls down some data, evaluates it, and makes new calls based on what it finds until it has all the metadata about all the photos in all the sets.
From now on, whenever you see a web service that you can invoke with a simple HTTP GET, remember that XSLT processors can do GETs, and they can do a lot with the XML that they get. Creating client applications that take full advantage of these web services may require no more technology than XSLT 1.0.