Example 8-11. Servlet filter for XSLT transformations
package com.oreilly.javaxslt.util;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
/**
* A utility class that uses the Servlet 2.3 Filtering API to apply
* an XSLT stylesheet to a servlet response.
*
* @author Eric M. Burke
*/
public class StylesheetFilter implements Filter {
private FilterConfig filterConfig;
private String xsltFileName;
/**
* This method is called once when the filter is first loaded.
*/
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
// xsltPath should be something like "/WEB-INF/xslt/a.xslt"
String xsltPath = filterConfig.getInitParameter("xsltPath");
if (xsltPath == null) {
throw new UnavailableException(
"xsltPath is a required parameter. Please "
+ "check the deployment descriptor.");
}
// convert the context-relative path to a physical path name
this.xsltFileName = filterConfig.getServletContext( )
.getRealPath(xsltPath);
// verify that the file exists
if (this.xsltFileName == null ||
!new File(this.xsltFileName).exists( )) {
throw new UnavailableException(
"Unable to locate stylesheet: " + this.xsltFileName, 30);
}
}
public void doFilter (ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
if (!(res instanceof HttpServletResponse)) {
throw new ServletException("This filter only supports HTTP");
}
BufferedHttpResponseWrapper responseWrapper =
new BufferedHttpResponseWrapper((HttpServletResponse) res);
chain.doFilter(req, responseWrapper);
// Tomcat 4.0 reuses instances of its HttpServletResponse
// implementation class in some scenarios. For instance, hitting
// reload( ) repeatedly on a web browser will cause this to happen.
// Unfortunately, when this occurs, output is never written to the
// BufferedHttpResponseWrapper's OutputStream. This means that the
// XML output array is empty when this happens. The following
// code is a workaround:
byte[] origXML = responseWrapper.getBuffer( );
if (origXML == null || origXML.length == 0) {
// just let Tomcat deliver its cached data back to the client
chain.doFilter(req, res);
return;
}
try {
// do the XSLT transformation
Transformer trans = StylesheetCache.newTransformer(
this.xsltFileName);
ByteArrayInputStream origXMLIn = new ByteArrayInputStream(origXML);
Source xmlSource = new StreamSource(origXMLIn);
ByteArrayOutputStream resultBuf = new ByteArrayOutputStream( );
trans.transform(xmlSource, new StreamResult(resultBuf));
res.setContentLength(resultBuf.size( ));
res.setContentType("text/html");
res.getOutputStream().write(resultBuf.toByteArray( ));
res.flushBuffer( );
} catch (TransformerException te) {
throw new ServletException(te);
}
}
/**
* The counterpart to the init( ) method.
*/
public void destroy( ) {
this.filterConfig = null;
}
}
This filter requires the deployment descriptor to provide the name of
the XSLT stylesheet as an initialization parameter. The following
line of code retrieves the parameter:
String xsltPath = filterConfig.getInitParameter("xsltPath");
By passing the stylesheet as a parameter, the filter can be
configured to work with any XSLT. Since the filter can be applied to
a servlet, JSP, or static file, the XML data is also completely
configurable.
The doFilter( ) method illustrates another
weakness of the current filtering API:
if (!(res instanceof HttpServletResponse)) {
throw new ServletException("This filter only supports HTTP");
}
Since there is no HTTP-specific filter interface, custom filters must
use instanceof and downcasts to ensure that only
HTTP requests are filtered.
Next, the filter creates the buffered response wrapper and delegates
to the next entry in the chain:
BufferedHttpResponseWrapper responseWrapper =
new BufferedHttpResponseWrapper((HttpServletResponse) res);
chain.doFilter(req, responseWrapper);
This effectively captures the XML output from the chain, making the
XSLT transformation possible. Before doing the transformation,
however, one "hack" is required to work with Tomcat 4.0:
byte[] origXML = responseWrapper.getBuffer( );
if (origXML == null || origXML.length == 0) {
// just let Tomcat deliver its cached data back to the client
chain.doFilter(req, res);
return;
}
Example 8-12. Filter deployment descriptor
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.3.dtd">
<web-app>
<filter>
<filter-name>xsltFilter</filter-name>
<filter-class>com.oreilly.javaxslt.util.StylesheetFilter</filter-class>
<init-param>
<param-name>xsltPath</param-name>
<param-value>/WEB-INF/xslt/templatePage.xslt</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>xsltFilter</filter-name>
<url-pattern>/home.xml</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>xsltFilter</filter-name>
<url-pattern>/company.xml</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>xsltFilter</filter-name>
<url-pattern>/jobs.xml</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>xsltFilter</filter-name>
<url-pattern>/products.xml</url-pattern>
</filter-mapping>
</web-app>
As the first few lines of the deployment descriptor indicate, filters
require Version 2.3 of the web application DTD.
The filter initialization parameter is specified next, inside of the
<filter> element. This provides the name of
the XSLT stylesheet for this particular filter instance. It is also
possible to specify multiple <filter>
elements using the same filter class but different filter names. This
allows the same web application to utilize a single filter with many
different configurations.
Finally, the deployment descriptor lists several explicit mappings
for this filter. In the examples shown, the filter is applied to
static XML files. It can just as easily be applied to a servlet or
JSP, however.