8.4. Servlet FiltersVersion 2.3 of the Java servlet specification adds a new feature called filters. A filter is an object that intercepts requests to a servlet, JSP, or static file in a web application. The filter has the opportunity to modify the request before passing it along to the underlying resource and can capture and modify the response before sending it back to the client. Since filters can be specified declaratively using the web application deployment descriptor, they can be inserted into existing web applications without altering any of the existing code. 8.4.1. Filter OverviewServlet filters are useful for many purposes, including logging, user authentication, data compression, encryption, and XSLT transformation. Many filters can be chained together, each performing one specific task. For the purposes of this book, XSLT transformations are the most interesting use of filters. Figure 8-5 illustrates the general filter architecture. Figure 8-5. Servlet filtersjavax.servlet.Filter is an interface that all custom filters must implement. It defines the following three methods: void init(FilterConfig config) void destroy( ) void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) The init( ) and destroy( ) methods are virtually identical to the init( ) and destroy( ) methods found in any servlet. init( ) is called when the filter is first loaded, and the FilterConfig parameter provides access to the ServletContext and to a list of initialization parameters. The code in Example 8-11 demonstrates each of these features. destroy( ), as expected, is called once when the filter is unloaded. This gives the filter a chance to release resources. The doFilter( ) method is called whenever a client request is received. The filter participates in a FilterChain set up by the servlet container, which allows multiple filters to be attached to one another. If this filter wishes to block the request, it can simply do nothing. Otherwise, it must pass control to the next resource in the chain: chain.doFilter(req, res); Although the next entry in the chain might be another filter, it is probably a servlet or a JSP. Either way, the filter does not have to know this. Simply calling doFilter(req, res) merely passes control to the next entry in the chain. To modify the request or response, the filter must modify the ServletRequest and/or ServletResponse object. Unfortunately, these are both interfaces, and their implementation classes are specific to each servlet container. Furthermore, the interfaces do not allow values to be modified. To facilitate this capability, Version 2.3 of the servlet API also adds wrapper classes that allow the request and response to be modified. The following new classes are now available: Each of these classes merely wraps around another request or response, and all methods merely delegate to the wrapped object. To modify behavior, programmers must extend from one of these classes and override one or more methods. Here is how a custom filter might look: public class MyFilter implements Filter { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // wrap around the original request and response MyRequestWrapper reqWrap = new MyRequestWrapper(req); MyResponseWrapper resWrap = new MyResponseWrapper(res); // pass the wrappers on to the next entry chain.doFilter(reqWrap, resWrap); } } In this case, MyRequestWrapper and MyResponseWrapper are doing the actual work of modifying the request and response. This works fine for many types of simple filters but is more complex when modifying the response content. To illustrate this point, consider the getOutputStream( ) method in javax.servlet.ServletResponse: public interface ServletResponse { ServletOutputStream getOutputStream( ) throws IOException; ...additional methods } Here is how javax.servlet.ServletResponseWrapper defines the same method: public class ServletResponseWrapper implements ServletResponse { private ServletResponse response; public ServletResponseWrapper(ServletResponse response) { this.response = response; } // default implementation delegates to the wrapped response public ServletOutputStream getOutputStream( ) throws IOException { return this.response.getOutputStream( ); } ...additional methods behave the same way } To modify the response sent to the client browser, the custom wrapper subclass must override the getOutputStream( ) method as follows: public class MyResponseWrapper extends ServletResponseWrapper { public ServletOutputStream getOutputStream( ) throws IOException { // cannot return the ServletOutputStream from the superclass, because // that object does not allow us to capture its output. Therefore, // return a custom subclass of ServletOutputStream: return new MyServletOutputStream( ); } } ServletOutputStream is an abstract class and does not provide methods that allow it to be modified. Instead, programmers must create custom subclasses of ServletOutputStream that allow them to capture the output and make any necessary modifications. This is what makes modification of the servlet response so difficult. 8.4.2. XSLT Transformation FilterThe previous discussion introduced a lot of concepts about servlet filters without a lot of details. Next, a complete example for performing XSLT transformations using a filter is presented. Hopefully this will illustrate some of the issues mentioned so far. The basic goal is to create a servlet filter that performs XSLT transformations. A servlet, JSP, or static XML file will provide the raw XML data. The filter will intercept this XML before it is sent to the client browser and apply an XSLT transformation. The result tree is then sent back to the browser. Example 8-9 is the first of three classes that comprise this example. This is a custom subclass of ServletOutputStream that captures its output in a byte array buffer. The XML data is queued up in this buffer as a first step before it is transformed. Example 8-9. BufferedServletOutputStream.javapackage com.oreilly.javaxslt.util; import java.io.*; import javax.servlet.*; /** * A custom servlet output stream that stores its data in a buffer, * rather than sending it directly to the client. * * @author Eric M. Burke */ public class BufferedServletOutputStream extends ServletOutputStream { // the actual buffer private ByteArrayOutputStream bos = new ByteArrayOutputStream( ); /** * @return the contents of the buffer. */ public byte[] getBuffer( ) { return this.bos.toByteArray( ); } /** * This method must be defined for custom servlet output streams. */ public void write(int data) { this.bos.write(data); } // BufferedHttpResponseWrapper calls this method public void reset( ) { this.bos.reset( ); } // BufferedHttpResponseWrapper calls this method public void setBufferSize(int size) { // no way to resize an existing ByteArrayOutputStream this.bos = new ByteArrayOutputStream(size); } } The BufferedServletOutputStream class extends directly from Servlet-OutputStream. The only abstract method in ServletOutputStream is write( ); therefore, our class must define that method. Instead of writing the data to the client, however, our class writes the data to a ByteArrayOutput-Stream . The remaining methods, reset( ) and setBufferSize( ), are required by the class shown in Example 8-10. Example 8-10. BufferedHttpResponseWrapper.javapackage com.oreilly.javaxslt.util; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /** * A custom response wrapper that captures all output in a buffer. */ public class BufferedHttpResponseWrapper extends HttpServletResponseWrapper { private BufferedServletOutputStream bufferedServletOut = new BufferedServletOutputStream( ); private PrintWriter printWriter = null; private ServletOutputStream outputStream = null; public BufferedHttpResponseWrapper(HttpServletResponse origResponse) { super(origResponse); } public byte[] getBuffer( ) { return this.bufferedServletOut.getBuffer( ); } public PrintWriter getWriter( ) throws IOException { if (this.outputStream != null) { throw new IllegalStateException( "The Servlet API forbids calling getWriter( ) after" + " getOutputStream( ) has been called"); } if (this.printWriter == null) { this.printWriter = new PrintWriter(this.bufferedServletOut); } return this.printWriter; } public ServletOutputStream getOutputStream( ) throws IOException { if (this.printWriter != null) { throw new IllegalStateException( "The Servlet API forbids calling getOutputStream( ) after" + " getWriter( ) has been called"); } if (this.outputStream == null) { this.outputStream = this.bufferedServletOut; } return this.outputStream; } // override methods that deal with the response buffer public void flushBuffer( ) throws IOException { if (this.outputStream != null) { this.outputStream.flush( ); } else if (this.printWriter != null) { this.printWriter.flush( ); } } public int getBufferSize( ) { return this.bufferedServletOut.getBuffer( ).length; } public void reset( ) { this.bufferedServletOut.reset( ); } public void resetBuffer( ) { this.bufferedServletOut.reset( ); } public void setBufferSize(int size) { this.bufferedServletOut.setBufferSize(size); } } BufferedHttpResponseWrapper is an extension of HttpServlet-ResponseWrapper and overrides all methods that affect the Writer or OutputStream back to the client. This allows us to fully capture and control the response before anything is sent back to the client browser. According to the servlet API, either getWriter( ) or getOutputStream( ) can be called, but not both. This custom response wrapper class cannot know which is needed, so it must support both. This is definitely an area where the servlet filtering API can make things a lot easier for programmers. WARNING: Very little of this is currently documented in the servlet specification. Perhaps this will improve by the time this book is published. However, there are currently very few examples that show how to capture and modify the response. Hopefully this will improve as more containers are upgraded to support the servlet 2.3 specification. The primary class in this example is shown in Example 8-11. This is the actual filter that performs XSLT transformations. Example 8-11. Servlet filter for XSLT transformationspackage 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; } The complete explanation is captured in the source code comments in Example 8-11. Basically, Tomcat seems to cache its response when the user tries to reload the same static file consecutive times. Without this check, the code fails because the origXML byte array is empty.[40]
Finally, the filter uses JAXP to perform the XSLT transformation, sending the result tree to the original servlet response. The deployment descriptor is listed in Example 8-12. 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. 8.4.3. Closing Thoughts on FiltersUsing filters for XSLT transformations is an interesting concept, primarily because it allows different stylesheets to be applied to XML from many different sources using the web application deployment descriptor. To use a different stylesheet, merely change the deployment descriptor. One interesting approach is using JSP to generate pure XML, then applying a filter to transform that XML into XHTML for the client. Filters do suffer drawbacks and probably are not the best solution for most applications. First and foremost, the filter API is available only in Version 2.3 of the servlet specification; many existing servlet containers do not support filters at all. In the case of XSLT transformations, a custom ServletOutputStream must be written to capture the response output, and downcasts are required because there is no HTTP-specific filter class. Because some servlet containers may cache the response for performance reasons, workarounds must be implemented to function reliably. Finally, this approach is slower than others. The XML must be converted into text and buffered in memory before the XSLT transformation can be performed, which is generally slower than sending SAX events or a DOM tree directly to the XSLT processor. Generating XML and performing the XSLT transformation in a servlet can avoid the extra conversions to and from text that filters require. Copyright © 2002 O'Reilly & Associates. All rights reserved. |
|