As seen in prior chapters, XSLT is a powerful tool for transforming an XML document from one XML syntax to another as well as transforming from XML to HTML. One interesting option enabled by the use of XSLT is offloading the transformation processing onto client apps, as most modern web browsers support XSLT. However, there are definite downsides to this offloading. Unless you're building apps for a limited and controlled audience, you will have little to no control as to how fast your users' client apps run. As a result, different users could have widely different experiences. Of course, this is also somewhat true with regular HTML. That being said, client-side transformations are a useful addition to any XML developer's toolbox.

Performing Client-Side Transformations

There are two main methods for performing a client-side transformation: processing instructions and client-side scripting.

Using processing instructions

The simplest way requesting an XSL transformation is to include an xml-stylesheet processing instruction between the XML declaration and the document's root element. For example, if a web browser receives a document such as in , it will make a request for and use that stylesheet to transform this document for display.

Example XML document with reference to stylesheet

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="http://web.archive.org/web/www.example.com/books.xsl"?>
<books>
 <book>
 <title>Ajax Hacks</title>
 <author>Bruce W. Perry</author>
 <pubDate>March 2006</pubDate>
 </book>
 <book>
 <title>LDAP System Administration</title>
 <author>Gerald Carter</author>
 <pubDate>March 2003</pubDate>
 </book>
 <book>
 <title>Java Servlet Programming</title>
 <author>Jason Hunter</author>
 <pubDate>April 2001</pubDate>
 </book>
</books>

The xml-stylesheet processing instruction can be limited to apply only when the XML document is to be displayed on a particular type of device. For example, many RSS feeds now use this processing instruction to display an HTML page when viewed through a browser. For example, the RSS feed from Wired includes this processing instruction:

<?xml-stylesheet href="http://feeds.wired.com/~d/styles/rss2full.xsl"
 type="text/xsl" media="screen"?>

As a result, when I view the feed () in a web browser (which is visible on my screen), I see the feed content as well as a variety of links to subscribe to the feed in my RSS aggregator of choice, as seen in .

Media-specific transformation

Java ScreenShot

When an RSS aggregator loads this same document, it knows to ignore the transformation because of the media portion of the processing instruction.

Transforming with JavaScript

In addition to using processing instructions for triggering a client-side transformation, Mozilla Firefox and Internet Explorer both support performing XSL transformations from JavaScript. This capability is explored further in the " section later in this chapter.

Performing Server-Side Transformations

As I mentioned above, there are some disadvantages to relying on client-side transformations. In addition to the performance issue, there is the simple fact that not all browsers support transformations. One option is to use client-side transformations on clients that support them and provide a server-side transformation otherwise.

Transforming in a filter

Using a Java servlet filter, it is possible to catch requests for XML documents from browsers that don't support client-side transformations and, if the document includes an xml-stylesheet processing instruction, perform the transformation on the server. Such a filter, which performs server-side transformations to support the Lynx text-mode browser, is contained in .

Example Transforming in a filter

package javaxml3.ch13;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class XSLFilter implements Filter {
 private Pattern hrefPattern;
 private SAXParserFactory saxParserFactory;
 private TransformerFactory transformerFactory;
 private Pattern typePattern;
 public void destroy( ) {
 }
 public void doFilter(ServletRequest request, ServletResponse response,
 FilterChain chain) throws IOException, ServletException {
 if (request instanceof HttpServletRequest) {
 if (isLynxRequest((HttpServletRequest) request)) {
 CharResponseWrapper wrapper = new CharResponseWrapper(
 (HttpServletResponse) response);
 chain.doFilter(request, wrapper);
 String xml = wrapper.toString( );
 // only parse XML responses
 if ("text/xml".equals(wrapper.getContentType( ))) {
 String stylesheetHref = getStylesheetHref(xml);
 if (stylesheetHref != null) {
 response.setContentType("text/html");
 transformResponse(response, xml, stylesheetHref);
 } else {
 response.getWriter( ).print(xml);
 }
 } else {
 response.getWriter( ).print(xml);
 }
 } else {
 chain.doFilter(request, response);
 }
 } else {
 chain.doFilter(request, response);
 }
 }
 private String getStylesheetHref(String xml) {
 StylesheetPIHandler handler = new StylesheetPIHandler( );
 try {
 SAXParser parser = saxParserFactory.newSAXParser( );
 InputSource input = new InputSource(new StringReader(xml));
 parser.parse(input, handler);
 return handler.getHref( );
 } catch (Exception e) {
 return null;
 }
 }
 public void init(FilterConfig config) throws ServletException {
 saxParserFactory = SAXParserFactory.newInstance( );
 transformerFactory = TransformerFactory.newInstance( );
 typePattern = Pattern.compile(".*type\\s*=\\s*\"(\\S*)\".*");
 hrefPattern = Pattern.compile(".*href\\s*=\\s*\"(\\S*)\".*");
 }
 private boolean isLynxRequest(HttpServletRequest request) {
 String userAgent = request.getHeader("User-Agent");
 return (userAgent.indexOf("Lynx") >= 0);
 }
 private void transformResponse(ServletResponse response, String xml,
 String stylesheetHref) throws IOException, ServletException {
 StreamSource sheetSource = new StreamSource(stylesheetHref);
 StreamSource xmlSource = new StreamSource(new StringReader(xml));
 StreamResult result = new StreamResult(response.getWriter( ));
 try {
 Transformer trans = transformerFactory.newTransformer(sheetSource);
 trans.transform(xmlSource, result);
 } catch (TransformerException e) {
 throw new ServletException("Unable to transform", e);
 }
 }
 class CharResponseWrapper extends HttpServletResponseWrapper {
 private CharArrayWriter output;
 public CharResponseWrapper(HttpServletResponse response) {
 super(response);
 output = new CharArrayWriter( );
 }
 public ServletOutputStream getOutputStream( ) throws IOException {
 return new ServletOutputStream( ) {
 public void write(int b) throws IOException {
 output.write(b);
 }
 };
 }
 public PrintWriter getWriter( ) {
 return new PrintWriter(output);
 }
 public String toString( ) {
 return output.toString( );
 }
 }
 class StylesheetPIHandler extends DefaultHandler {
 private String href = null;
 public String getHref( ) {
 return href;
 }
 public void processingInstruction(String target, String data)
 throws SAXException {
 if ("xml-stylesheet".equals(target)) {
 Matcher typeMatcher = typePattern.matcher(data);
 if (typeMatcher.matches( )
 && "text/xsl".equals(typeMatcher.group(1))) {
 Matcher hrefMatcher = hrefPattern.matcher(data);
 if (hrefMatcher.matches( ))
 href = hrefMatcher.group(1);
 }
 }
 }
 }
}

As you can see, this filter intercepts requests; if it's possible that the response will need to be transformed, it replaces the current HttpServletResponse object with a wrapper object that buffers the response output. Once the response has been generated, a SAX parser is used to find processing instructions, and then two regular expressions are used to discover the appropriate stylesheet URI. With this filter in place, a request for books.xml will return HTML to Lynx and XML to other browsers. The outputs, seen in Figures and , contain renderings of the same HTML in Lynx and Internet Explorer, respectively. The only difference is where that HTML was generated.

Result of server-side transformation in Lynx

Java ScreenShot

Result of client-side transformation in Internet Explorer

Java ScreenShot

Transforming with JSTL

As noted above, JSTL includes a TRansform custom tag that allows you to perform transformations of XML documents. It needs both an XML document and a stylesheet document. These documents can be defined in the page:

<c:set var="doc">
 <books>
 <book>
 <title>Ajax Hacks</title>
 <author>Bruce W. Perry</author>
 <pubdate>March 2006</pubdate>
 </book>
 <!-- add more tutorials here -->
 </books>
</c:set>
<c:set var="xsl">
 <xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="html" />
 <xsl:template match="books">
 <html>
 <head>
 <title>Books</title>
 </head>
 <body>
 <h1>Book List</h1>
 <table>
 <tbody>
 <tr>
 <th>Title</th>
 <th>Author</th>
 <th>Publication Date</th>
 </tr>
 <xsl:apply-templates select="book" />
 </tbody>
 </table>
 </body>
 </html>
 </xsl:template>
 <xsl:template match="book">
 <tr>
 <td><xsl:value-of select="title" /></td>
 <td><xsl:value-of select="author" /></td>
 <td><xsl:value-of select="pubdate" /></td>
 </tr>
 </xsl:template>
 </xsl:stylesheet>
</c:set>
<x:transform doc="${doc}" xslt="${xsl}" />

The documents can also be loaded from URLs:

<%@ taglib prefix="x" uri="http://java.oracle.com/jsp/jstl/xml"%>
<%@ taglib prefix="c" uri="http://java.oracle.com/jsp/jstl/core"%>
<c:import var="doc" url="http://localhost:8080/ch13-servlet/books.xml"/>
<c:import var="xsl" url="http://localhost:8080/ch13-servlet/books.xsl"/>
<x:transform doc="${doc}" xslt="${xsl}" />

The XML can also be provided as the body of the transform tag:

<%@ taglib prefix="x" uri="http://java.oracle.com/jsp/jstl/xml"%>
<%@ taglib prefix="c" uri="http://java.oracle.com/jsp/jstl/core"%>
<c:import var="xsl" url="http://localhost:8080/ch13-servlet/books.xsl"/>
<x:transform xslt="${xsl}">
 <books>
 <book>
 <title>Ajax Hacks</title>
 <author>Bruce W. Perry</author>
 <pubdate>March 2006</pubdate>
 </book>
 <!-- add more tutorials here -->
 </books>
</x:transform>

Finally, the result of the transformation can be saved as a DOM object against which XPath expressions can be evaluated:

<%@ taglib prefix="x" uri="http://java.oracle.com/jsp/jstl/xml"%>
<%@ taglib prefix="c" uri="http://java.oracle.com/jsp/jstl/core"%>
<c:import var="xsl" url="http://localhost:8080/ch13-servlet/books.xsl"/>
<x:transform xslt="${xsl}" var="result">
 <books>
 <book>
 <title>Ajax Hacks</title>
 <author>Bruce W. Perry</author>
 <pubdate>March 2006</pubdate>
 </book>
 <!-- add more tutorials here -->
 </books>
</x:transform>
<x:out select="$result//h1"/>