Section 15.4. Servlet Filters


15.4. Servlet Filters

The servlet Filter API generalizes the Java Servlet API to allow modular component "filters" to operate on the servlet request and responses in a sort of pipeline. Filters are chained, meaning that when more than one filter is applied, the servlet request is passed through each filter in succession, with each having an opportunity to act upon or modify the request before passing it to the next filter. Similarly, upon completion, the servlet result is effectively passed back through the chain on its return trip to the browser. Servlet filters may operate on any requests to a web application, not just those handled by the servlets; they may filter static content, as well. As of Servlet API 2.4, you can also control whether filters are applied to error and welcome pages as well as pages forwarded or included using the request dispatcher.

Filters are declared and mapped to servlets in the web.xml file. There are two ways to map a filter: using a URL pattern like those used for servlets or by specifying a servlet by its instance name (<servlet-name>). Filters obey the same basic rules as servlets when it comes to URL matching, but when multiple filters match a path, they are all invoked.

The order of the chain is determined by the order in which matching filter mappings appear in the web.xml file, with <url-pattern> matches taking precedence over <servlet-name> matches. This is contrary to the way servlet URL matching is done, with specific matches taking the highest priority. Filter chains are constructed as follows. First, each filter with a matching URL pattern is called in the order in which it appears in the web.xml file; next, each filter with a matching servlet name is called, also in order of appearance. URL patterns take a higher priority than filters specifically associated with a servlet, so in this case, patterns such as /* have first crack at an incoming request.

The Filter API is very simple and mimics the Servlet API. A servlet filter implements the javax.servlet.Filter interface and implements three methods: init( ), doFilter( ), and destroy( ). The doFilter( ) method is where the work is performed. For each incoming request, the ServletRequest and ServletResponse objects are passed to doFilter( ). Here, we have a chance to examine and modify these objectsor even substitute our own objects for thembefore passing them to the next filter and, ultimately, the servlet (or user) on the other side. Our link to the rest of the filter chain is another parameter of doFilter( ), the FilterChain object. With FilterChain, we can invoke the next element in the pipeline. The following section presents an example.

15.4.1. A Simple Filter

For our first filter, we'll do something easy but practical: create a filter that limits the number of concurrent connections to its URLs. We'll simply have our filter keep a counter of the active connections passing through it and turn away new requests when they exceed a specified limit:

     import java.io.*;     import javax.servlet.*;     import javax.servlet.http.*;     public class ConLimitFilter implements Filter     {         int limit;         int count;         public void init( FilterConfig filterConfig )             throws ServletException         {             String s = filterConfig.getInitParameter("limit");             if ( s == null )                 throw new ServletException("Missing init parameter: "+limit);             limit = Integer.parseInt( s );         }         public void doFilter (             ServletRequest req, ServletResponse res, FilterChain chain )                 throws IOException, ServletException         {             if ( count > limit ) {                 HttpServletResponse httpRes = (HttpServletResponse)res;                 httpRes.sendError(                     httpRes.SC_SERVICE_UNAVAILABLE, "Too Busy.");             } else {                 ++count;                 chain.doFilter( req, res );                 --count;             }         }         public void destroy(  ) { }     } 

ConLimitFilter implements the three life-cycle methods of the Filter interface: init( ), doFilter( ), and destroy( ). In our init( ) method, we use the FilterConfig object to look for an initialization parameter named "limit" and turn it into an integer. Users can set this value in the section of the web.xml file where the instance of our filter is declared. The doFilter( ) method implements all our logic. First, it receives ServletRequest and ServletResponse object pairs for incoming requests. Depending on the counter, it then either passes them down the chain by invoking the next doFilter( ) method on the FilterChain object, or rejects them by generating its own response. We use the standard HTTP message "504 Service Unavailable" when we deny new connections.

Calling doFilter( ) on the FilterChain object continues processing by invoking the next filter in the chain or by invoking the servlet if ours is the last filter. Alternatively, when we choose to reject the call, we use the ServletResponse to generate our own response and then simply allow doFilter( ) to exit. This stops the processing chain at our filter, although any filters called before us still have an opportunity to intervene as the request effectively traverses back to the client.

Notice that ConLimitFilter increments the count before calling doFilter( ) and decrements it after. Prior to calling doFilter( ), we can work on the request before it reaches the rest of the chain and the servlet. After the call to doFilter( ), the chain to the servlet has completed, and the request is sent back to the client. This is our opportunity to do any post-processing of the response.

Finally, we should mention that although we've been talking about the servlet request and response as if they were HttpServletRequest and HttpServletResponse, the doFilter( ) method actually takes the more generic ServletRequest and ServletResponse objects as parameters. As filter implementers, we are expected to determine when it is safe to treat them as HTTP traffic and perform the cast as necessary (which we do here in order to use the sendError( ) HTTP response method).

15.4.2. A Test Servlet

Before we go on, here is a simple test servlet you can use to try out this filter and the other filters we'll develop in this section. It's called WaitServlet and, as its name implies, it simply waits. You can specify how long it waits as a number of seconds with the servlet parameter time.

     import java.io.*;     import javax.servlet.*;     import javax.servlet.http.*;     public class WaitServlet extends HttpServlet     {         public void doGet( HttpServletRequest request, HttpServletResponse response)             throws ServletException, IOException         {             String waitStr = request.getParameter("time");             if ( waitStr == null )                 throw new ServletException("Missing parameter: time");             int wait = Integer.parseInt(waitStr);             try {                 Thread.sleep( wait * 1000 );             } catch( InterruptedException e ) {                 throw new ServletException(e);             }             response.setContentType("text/html");             PrintWriter out = response.getWriter(  );             out.println(                 "<html><body><h1>WaitServlet Response</h1></body></html>");             out.close(  );         }     } 

By making multiple simultaneous requests to the WaitServlet, you can try out the ConLimitFilter. Be careful, though, because some web browsers (namely Opera) won't open multiple requests to the same URL. You may have to add extraneous parameters to trick the web browser. To try out in full, see the learningjava.war application.

15.4.3. Declaring and Mapping Filters

Filters are declared and mapped in the web.xml file much as servlets are. Like servlets, one instance of a filter class is created for each filter declaration in the web.xml file. A filter declaration looks like this:

     <filter>         <filter-name>defaultsfilter1</filter-name>         <filter-class>RequestDefaultsFilter</filter-class>     </filter> 

It specifies a filter handle name to be used for reference within the web.xml file and the filter's Java class name. Filter declarations may also contain <init-param> parameter sections, just like servlet declarations.

Filters are mapped to resources with <filter-mapping> declarations that specify the filter handle name and either the specific servlet handle name or a URL pattern, as we discussed earlier:

     <filter-mapping>         <filter-name>conlimitfilter1</filter-name>         <servlet-name>waitservlet1</servlet-name>      </filter-mapping>     <filter-mapping>         <filter-name>conlimitfilter1</filter-name>         <url-pattern>/*</url-pattern>      </filter-mapping> 

Filter mappings appear after all filter declarations in the web.xml file.

15.4.4. Filtering the Servlet Request

Our first filter example was not very exciting because it did not actually modify any information going to or coming from the servlet. Next, let's do some actual "filtering" by modifying the incoming request before it reaches a servlet. In this example, we'll create a request "defaulting" filter that automatically supplies default values for specified servlet parameters when they are not provided in the incoming request. Despite its simplicity, this example might be very useful. Here is the RequestDefaultsFilter:

     import java.io.*;     import javax.servlet.*;     import javax.servlet.http.*;     public class RequestDefaultsFilter implements Filter     {         FilterConfig filterConfig;         public void init( FilterConfig filterConfig ) throws ServletException         {             this.filterConfig = filterConfig;         }         public void doFilter (             ServletRequest req, ServletResponse res, FilterChain chain )                 throws IOException, ServletException         {             WrappedRequest wrappedRequest =                 new WrappedRequest( (HttpServletRequest)req );             chain.doFilter( wrappedRequest, res );         }         public void destroy(  ) { }         class WrappedRequest extends HttpServletRequestWrapper         {             WrappedRequest( HttpServletRequest req ) {                 super( req );             }             public String getParameter( String name ) {                 String value = super.getParameter( name );                 if ( value == null )                     value = filterConfig.getInitParameter( name );                 return value;             }         }     } 

To interpose ourselves in the data flow, we must do something drastic. We kidnap the incoming HttpServletRequest object and replace it with an evil twin that does our bidding. The technique, which we'll use here for modifying the request object and later for modifying the response, is to wrap the real request with an adapter, allowing us to override some of its methods. Here, we will take control of the HttpServletRequest's getParameter( ) method, modifying it to look for default values where it would otherwise return null.

Again, we implement the three life-cycle methods of Filter, but this time, before invoking doFilter( ) on the filter chain to continue processing, we wrap the incoming HttpServletRequest in our own class, WrappedRequest. WrappedRequest extends a special adapter called HttpServletRequestWrapper. This wrapper class is a convenience utility that extends HttpServletRequest. It accepts a reference to a target HttpServletRequest object and, by default, delegates all of its methods to that target. This makes it very convenient for us to simply override one or more methods of interest to us. All we have to do is override getParameter( ) in our WrappedRequest class and add our functionality. Here, we simply call our parent's getParameter( ), and in the case where the value is null, we try to substitute a filter initialization parameter of the same name.

Try this example using the WaitServlet with a filter declaration and mapping as follows:

     <filter>         <filter-name>defaultsfilter1</filter-name>         <filter-class>RequestDefaultsFilter</filter-class>         <init-param>             <param-name>time</param-name>             <param-value>3</param-value>         </init-param>     </filter>     ...     <filter-mapping>         <filter-name>defaultsfilter1</filter-name>         <servlet-name>waitservlet1</servlet-name>      </filter-mapping> 

Now the WaitServlet receives a default time value of three seconds even when you don't specify one.

15.4.5. Filtering the Servlet Response

Filtering the request was fairly easy, and we can do something similar with the response object using exactly the same technique. There is a corresponding HttpServletResponseWrapper that we can use to wrap the response before the servlet uses it to communicate back to the client. By wrapping the response, we can intercept methods that the servlet uses to write the response, just as we intercepted the getParameter( ) method that the servlet used in reading the incoming data. For example, we could override the sendError( ) method of the HttpServletResponse object and modify it to redirect to a specified page. In this way, we could create a servlet filter that emulates the programmable error page control offered in the web.xml file. But the most interesting technique available to us, and the one we'll show here, involves actually modifying the data written by the servlet before it reaches the client. To do this we have to pull a double "switcheroo." We wrap the servlet response to override the getWriter( ) method and then create our own wrapper for the client's PrintWriter object supplied by this method, one that buffers the data written and allows us to modify it. This is a useful and powerful technique, but it can be tricky.

Our example, LinkResponseFilter, is an automatic hyperlink-generating filter that reads HTML responses and searches them for patterns supplied as regular expressions. When it matches a pattern, it turns it into an HTML link. The pattern and links are specified in the filter initialization parameters. You could extend this example with access to a database or XML file and add additional rules to make it into a very powerful site-management helper. Here it is:

     import java.io.*;     import java.util.*;     import javax.servlet.*;     import javax.servlet.http.*;     public class LinkResponseFilter implements Filter     {         FilterConfig filterConfig;         public void init( FilterConfig filterConfig )             throws ServletException         {             this.filterConfig = filterConfig;         }         public void doFilter (             ServletRequest req, ServletResponse res, FilterChain chain )                 throws IOException, ServletException         {             WrappedResponse wrappedResponse =                 new WrappedResponse( (HttpServletResponse)res );             chain.doFilter( req, wrappedResponse );             wrappedResponse.close(  );         }         public void destroy(  ) { }         class WrappedResponse extends HttpServletResponseWrapper         {             boolean linkText;             PrintWriter client;             WrappedResponse( HttpServletResponse res ) {                 super( res );             }             public void setContentType( String mime ) {                 super.setContentType( mime );                 if ( mime.startsWith("text/html") )                     linkText = true;             }             public PrintWriter getWriter(  ) throws             IOException {                 if ( client == null )                     if ( linkText )                         client = new LinkWriter(                             super.getWriter(  ), new ByteArrayOutputStream(  ) );                     else                         client = super.getWriter(  );                 return client;             }             void close(  ) {                 if ( client != null )                     client.close(  );             }         }         class LinkWriter extends PrintWriter         {             ByteArrayOutputStream buffer;             Writer client;             LinkWriter( Writer client, ByteArrayOutputStream buffer ) {                 super( buffer );                 this.buffer = buffer;                 this.client = client;             }             public void close(  ) {                 try {                     flush(  );                     client.write( linkText( buffer.toString(  ) ) );                     client.close(  );                 } catch ( IOException e ) {                     setError(  );                 }             }             String linkText( String text ) {                 Enumeration en = filterConfig.getInitParameterNames(  );                 while ( en.hasMoreElements(  ) ) {                     String pattern = (String)en.nextElement(  );                     String value = filterConfig.getInitParameter( pattern );                     text = text.replaceAll(                         pattern, "<a href="+value+">$0</a>" );                 }                 return text;             }         }     } 

That was a bit longer than our previous examples, but the basics are the same. We have wrapped the HttpServletResponse object with our own WrappedResponse class using the HttpServletResponseWrapper helper class. Our WrappedResponse overrides two methods: getWriter( ) and setContentType( ). We override setContentType( ) in order to set a flag indicating whether the output is of type "text/html" (an HTML document). We don't want to be performing regular-expression replacements on binary data such as images, for example, should they happen to match our filter. We also override getWriter( ) to provide our substitute writer stream, LinkWriter. Our LinkWriter class is a PrintStream that takes as arguments the client PrintWriter and a ByteArrayOutputStream that serves as a buffer for storing output data before it is written. We are careful to substitute our LinkWriter only if the linkText Boolean set by setContent( ) is true. When we do use our LinkWriter, we cache the stream so that any subsequent calls to getWriter( ) return the same object. Finally, we have added one method to the response object: close( ). A normal HttpServletResponse does not have a close( ) method. We use ours on the return trip to the client to indicate that the LinkWriter should complete its processing and write the actual data to the client. We do this in case the client does not explicitly close the output stream before exiting the servlet service methods.

This explains the important parts of our filter-writing example. Let's wrap up by looking at the LinkWriter, which does the magic in this example. LinkWriter is a PrintStream that holds references to two other Writers: the true client PrintWriter and a ByteArrayOutputStream. The LinkWriter calls its superclass constructor, passing the ByteArrayOutputStream as the target stream, so all of its default functionality (its print( ) methods) writes to the byte array. Our only real job is to intercept the close( ) method of the PrintStream and add our text linking before sending the data. When LinkWriter is closed, it flushes itself to force any data buffered in its superclass out to the ByteArrayOutputStream. It then retrieves the buffered data (with the ByteArrayOutputStream toString( ) method) and invokes its linkText( ) method to create the hyperlinks before writing the linked data to the client. The linkText( ) method simply loops over all the filter initialization parameters, treating them as patterns, and uses the String replaceAll( ) method to turn them into hyperlinks. (See Chapter 1 for more about replaceAll( ).)

This example works, but it has limitations. First, we cannot buffer an infinite amount of data. A better implementation would have to make a decision about when to start writing data to the client, potentially based on the client-specified buffer size of the HttpServletResponse API. Next, our implementation of linkText( ) could probably be speeded up by constructing one large regular expression using alternation. You will no doubt find other ways it can be improved.



    Learning Java
    Learning Java
    ISBN: 0596008732
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net