Chapter 10

Filters

A filter is an object that intercepts a request and processes the ServletRequest or the ServletResponse passed to the resource being requested. Filters can be used for logging, encryption and decryption, session checking, image transformation, and so on. Filters can be configured to intercept a single resource or multiple resources. Filter configuration can be done through annotations or the deployment descriptor. If multiple filters are applied to the same resource or the same set of resources, the invocation order is sometimes important and in this case you need the deployment descriptor.

This chapter explains how to write and register filters. Several examples are also given.

The Filter API

The following section explains the interfaces that are used in a filter, including Filter, FilterConfig, and FilterChain.

A filter class must implement the javax.servlet.Filter interface. This interface exposes three lifecycle methods for a filter: init, doFilter, and destroy.

The init method is called by the servlet container when the filter is being put into service, i.e. when the application is started. In other words, init does not wait until a resource associated with the filter is invoked. This method is only called once and should contain initialization code for the filter. The signature of init is as follows.

void init(FilterConfig filterConfig)

Note that the servlet container passes a FilterConfig to the init method. The FilterConfig interface is explained below.

The doFilter method of a Filter instance is called by the servlet container every time a resource associated with the filter is invoked. This method receives a ServletRequest, a ServletResponse, and a FilterChain.

Here is the signature of doFilter:

void doFilter(ServletRequest request, ServletResponse response, 
        FilterChain filterChain)

As you can see here, an implementation of doFilter has access to the ServletRequest and the ServletResponse. As such, it is possible to add an attribute to the ServletRequest or a header to the ServletResponse. You can even decorate the ServletRequest or the ServletResponse to change their behavior, as explained in Chapter 17, “Decorating Requests and Responses.”

The last line of code in a doFilter method implementation should be a call to the doChain method on the FilterChain passed as the third argument to doFilter:

filterChain.doFilter(request, response)

A resource may be associated with multiple filters (or in a more technical term, a chain of filters), and FilterChain.doFilter() basically causes the next filter in the chain to be invoked. Calling FilterChain.doFilter() on the last filter in the chain causes the resource itself to be invoked.

If you do not call FilterChain.doFilter() at the end of your Filter.doFilter() implementation, processing will stop there and the resource will not be invoked.

Note that doFilter is the only method in the FilterChain interface. This method is slightly different from the doFilter method in Filter. In FilterChain, doFilter only take two arguments instead of three.

The last lifecycle method in Filter is destroy. Here is its signature.

void destroy()

This method is called by the servlet container before the filter is taken out of service, which normally happens when the application is stopped.

Unless a filter class is declared in multiple filter element in the deployment descriptor, the servlet container will only create a single instance of each type of filter. Since a servlet/JSP application is normally a multi-user application, a filter instance may be accessed by multiple threads at the same time and you need to handle multi-threading issues carefully. For an example of how to handle thread safety, see the Download Counter filter example later in this chapter.

Filter Configuration

After you finish writing a filter class, you still have to configure the filter. Filter configuration has these objectives:

  • Determine what resources the filter will intercept.
  • Setup initial values to be passed to the filter’s init method.
  • Give the filter a name. Giving a filter a name in most cases does not have much significance but can sometimes be helpful. For example, you might want to log the time a filter starts, and if you have multiple filters in the same application, it is often useful to see the order in which the filters are invoked.

The FilterConfig interface allows you to access the ServletContext through its getServletContext method.

ServletContxt getServletContext()

If a filter has a name, the getFilterName method in FilterConfig returns the name. Here is the signature of getFilterName.

java.lang.String getFilterName()

However, you are most often interested in getting initial parameters, which are initialization values that the developer or deployer passes to the filter. There are two methods you can use to handle initial parameters, the first of which is getParameterNames:

java.util.Enumeration<java.lang.String> getInitParameterNames()

This method returns an Enumeration of filter parameter names. If no parameter exists for the filter, this method returns an empty Enumeration.

The second method for handling initial parameters is getParameter:

java.lang.String getInitParameter(java.lang.String parameterName)

There are two ways to configure a filter. You can configure a filter using the WebFilter annotation type or by registering it in the deployment descriptor. Using @WebFilter is easy as you just need to annotate the filter class and you do not need the deployment descriptor. However, changing the configuration settings requires that the filter class be recompiled. On the other hand, configuring a filter in the deployment descriptor means changing configuration values is a matter of editing a text file.

To use @WebFilter, you need to be familiar with its attributes. Table 10.1 lists attributes that may appear in the WebFilter annotation type. All attributes are optional.

Attribute

Description

asyncSupported

Specifies whether or not the filter supports the asynchronous operation mode

description

The description of the filter

dispatcherTypes

The dispatcher types to which the filter applies

displayName

The display name of the filter

filterName

The name of the filter

initParams

The initial parameters

largeIcon

The name of the large icon for the filter

servletNames

The names of the servlets to which the filter applies

smallIcon

The name of the small icon for the filter

urlPatterns

The URL patterns to which the filter applies

value

The URL patterns to which the filter applies

Table 10.1: WebFilter attributes

For instance, the following @WebFilter annotation specifies that the filter name is DataCompressionFilter and it applies to all resources.

@WebFilter(filterName="DataCompressionFilter", urlPatterns={"/*"}) 

This is equivalent to declaring these filter and filter-mapping elements in the deployment descriptor.

<filter>
    <filter-name>DataCompressionFilter</filter-name>
    <filter-class>
        the fully-qualified name of the filter class
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>DataCompresionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

As another example, the following filter is passed two initial parameters:

@WebFilter(filterName = "Security Filter", urlPatterns = { "/*" },
        initParams = {
            @WebInitParam(name = "frequency", value = "1909"),
            @WebInitParam(name = "resolution", value = "1024") 
        }
)

Using the filter and filter-mapping elements in the deployment descriptor, the configuration settings would be as follows.

<filter>
    <filter-name>Security Filter</filter-name>
    <filter-class>filterClass</filter-class>
    <init-param>
        <param-name>frequency</param-name>
        <param-value>1909</param-value>
     </init-param>
    <init-param>
        <param-name>resolution</param-name>
        <param-value>1024</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>DataCompresionFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

The deployment descriptor is discussed in detail in Chapter 20, “Deployment.”

Example 1: Logging Filter

As the first example, consider a simple filter in the filterdemo application that is used to log request URIs in a text file. The name of the text file is configurable through an initial parameter. In addition, each entry in the log can be prefixed with a preset string that is also passed as an initial parameter. You can derive valuable information from the log, such as which resource in your application is the most popular or at what time of the day your web site is most busy.

The filter class is called LoggingFilter and is printed in Listing 10.1. By convention, a filter class name ends with Filter.

Listing 10.1: The LoggingFilter class

package filterdemo;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;

@WebFilter(filterName = "LoggingFilter", urlPatterns = { "/*" },
        initParams = {
                @WebInitParam(name = "logFileName",
                        value = "log.txt"),
                @WebInitParam(name = "prefix", value = "URI: ") })
public class LoggingFilter implements Filter {

    private PrintWriter logger;
    private String prefix;

    @Override
    public void init(FilterConfig filterConfig)
            throws ServletException {
        prefix = filterConfig.getInitParameter("prefix");
        String logFileName = filterConfig
                .getInitParameter("logFileName");
        String appPath = filterConfig.getServletContext()
                .getRealPath("/");
        // without path info in logFileName, the log file will be
        // created in $TOMCAT_HOME/bin

        System.out.println("logFileName:" + logFileName);
        try {
            logger = new PrintWriter(new File(appPath,
                    logFileName));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        }
    }

    @Override
    public void destroy() {
        System.out.println("destroying filter");
        if (logger != null) {
            logger.close();
        }
    }

    @Override
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("LoggingFilter.doFilter");
        HttpServletRequest httpServletRequest = 
                (HttpServletRequest) request;
        logger.println(new Date() + " " + prefix
                + httpServletRequest.getRequestURI());
        logger.flush();
        filterChain.doFilter(request, response);
    }
}

Let’s look at the filter class closely.

First and foremost, the filter class implements the Filter interface and declares two variables, a PrintWriter called logger and a String named prefix:

    private PrintWriter logger;
    private String prefix;

The PrintWriter will be used for writing to a text file. The prefix String will be used as the prefix for each log entry.

The filter class is annotated with @WebFilter, passing two initial parameters (logFileName and prefix) to the filter:

@WebFilter(filterName = "LoggingFilter", urlPatterns = { "/*" },
        initParams = {
                @WebInitParam(name = "logFileName",
                        value = "log.txt"),
                @WebInitParam(name = "prefix", value = "URI: ") 
        }
)

The init method calls the getInitParameter method on the passed-in FilterConfig to retrieve the prefix and logFileName initial parameters. The value of the prefix parameter is assigned to the class-level variable prefix and logFileName is used to create a PrintWriter.

        prefix = filterConfig.getInitParameter("prefix");
        String logFileName = filterConfig
                .getInitParameter("logFileName");

Since servlet/JSP applications are started by the servlet/JSP container, the current working directory is the location in which java was invoked. In Tomcat this is the bin directory of the Tomcat installation. To create a log file in the application directory, you need the absolute path to it. You can retrieve it using the ServletContext.getRealPath method. Combining the application path and the logFileName initial parameter, you have this:

        String appPath = filterConfig.getServletContext()
                .getRealPath("/");
        // without path info in logFileName, the log file will be
        // created in $TOMCAT_HOME/bin

        try {
            logger = new PrintWriter(new File(appPath,
                    logFileName));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            throw new ServletException(e.getMessage());
        }

A log file will be created when the init method is executed. If a file with the same name already exists in the application directory, the file will be overwritten by the new file.

When the application is shut down, the PrintWriter must be closed. Therefore, in the filter’s destroy method, you write

        if (logger != null) {
            logger.close();
        }

The doFilter method logs all requests by downcasting the ServletRequest to HttpServletRequest and calling its getRequestURI method. The result of getRequestURI is then fed to the println method of the PrintWriter.

        HttpServletRequest httpServletRequest = 
                (HttpServletRequest) request;
        logger.println(new Date() + " " + prefix
                + httpServletRequest.getRequestURI());

Each entry is given a timestamp and a prefix to make it easy to figure out when an entry is made. The doFilter method then flushes the PrintWriter and calls FilterChain.doFilter to invoke the resource.

        logger.flush();
        filterChain.doFilter(request, response);

When you run Tomcat, the filter will be put in service without waiting for the first request. You should see printed on your console the value of the logFileName parameter.

To test this filter, invoke the test.jsp page in Listing 10.2 using this URL:

http://localhost:8080/filterdemo/test.jsp

Listing 10.2: The test.jsp page

<!DOCTYPE html>
<html>
    <head><title>Testing LoggingFilter</title></head>
    <body>
        Test...
    </body>
</html>

Verify that the filter is working correctly by examining the content of the log file.

Example 2: Image Protector Filter

The Image Protector Filter in this example prevents an image from being downloaded by typing the image URL in the browser’s Address box. An image in the application will only show if the link to the image is clicked on a page. The filter works by checking the value of the referer HTTP header. A null value means the current request has no referrer, in other words the resource is being requested directly by typing its URL. A resource with a non-null referer header will have the page of origin as its referrer. Note that the header name is spelled with one r between the second e and the third e.

The filter class, ImageProtectorFilter, is given in Listing 10.3. From the WebFilter annotation you know that the filter is applied to all resources having png, jpg, or gif extension.

Listing 10.3: The ImageProtectorFilter class

package filterdemo;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter(filterName = "ImageProtectorFilter", urlPatterns = {
        "*.png", "*.jpg", "*.gif" })
public class ImageProtectorFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig)
            throws ServletException {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        System.out.println("ImageProtectorFilter");
        HttpServletRequest httpServletRequest = 
                (HttpServletRequest) request;
        String referrer = httpServletRequest.getHeader("referer");
        System.out.println("referrer:" + referrer);
        if (referrer != null) {
            filterChain.doFilter(request, response);
        } else {
            throw new ServletException("Image not available");
        }
    }
}

The init and destroy methods are empty. The doFilter method reads the value of the referer header and either invokes the resource or throws an exception:

        String referrer = httpServletRequest.getHeader("referer");
        System.out.println("referrer:" + referrer);
        if (referrer != null) {
            filterChain.doFilter(request, response);
        } else {
            throw new ServletException("Image not available");
        }

To test the filter, try opening the logo.png image by typing this URL in your browser’s Address box:

http://localhost:8080/filterdemo/image/logo.png

You’ll get an “Image not available” error message.

Now, invoke the image.jsp page in Listing 10.4 using this URL:

http://localhost:8080/filterdemo/image.jsp

You should see the image. The reason why this works is because the image.jsp page contains this link that instructs the browser to download the image. When the browser tried to download the image, it also sent the URL of the page (in this case, http://localhost:8080/filterdemo/image.jsp) in the referer header.

Listing 10.4: The image.jsp page

<!DOCTYPE html>
<html>
    <head><title>Get Image</title></head>
    <body>
        <img src='image/logo.png'/>        
    </body>
</html>

Example 3: Download Counter Filter

The Download Counter filter features a filter that counts how many times a resource has been downloaded. This is particularly useful if you want to know how popular your documents or your videos are. The numbers are stored in a properties file and not in a database for simplicity’s sake. The resource URI is used as the property key in the properties file.

Because we’re storing values in a properties file and a filter can be accessed by multiple threads at the same time, there is a thread-safety issue that needs to be resolved. A user can request a resource and the filter will need to read the corresponding property value, increment it by one, and store back the new value. What if a second user requests the same resource before the first thread finishes its business? Well, the count will be inaccurate. Synchronizing the code that reads and writes values does not sound like an ideal solution as scalability may suffer.

This example shows how to resolve this thread-safety problem by using a Queue and an Executor. If you're not familiar with these two Java types, please refer to my other book, "Java: A Beginner's Tutorial (Fourth Edition)."

In short, all incoming requests place a task in a queue in a single-threaded Executor. Placing a task is fast because it’s an asynchronous operation so that you don’t have to wait for the task to be complete. The Executor will take one item at a time from the queue and increment the correct property. Since the Executor uses only one thread, we have eliminated any chance of multiple threads accessing the property file.

The DownloadCounterFilter class is given in Listing 10.5.

Listing 10.5: DownloadCounterFilter

package filterdemo;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter(filterName = "DownloadCounterFilter",
        urlPatterns = { "/*" })
public class DownloadCounterFilter implements Filter {

    ExecutorService executorService = Executors
            .newSingleThreadExecutor();
    Properties downloadLog;
    File logFile;

    @Override
    public void init(FilterConfig filterConfig)
            throws ServletException {
        System.out.println("DownloadCounterFilter");
        String appPath = filterConfig.getServletContext()
                .getRealPath("/");
        logFile = new File(appPath, "downloadLog.txt");
        if (!logFile.exists()) {
            try {
                logFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        downloadLog = new Properties();
        try {
            downloadLog.load(new FileReader(logFile));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void destroy() {
        executorService.shutdown();
    }

    @Override
    public void doFilter(ServletRequest request,
            ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        final String uri = httpServletRequest.getRequestURI();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                String property = downloadLog.getProperty(uri);
                if (property == null) {
                    downloadLog.setProperty(uri, "1");
                } else {
                    int count = 0;
                    try {
                        count = Integer.parseInt(property);
                    } catch (NumberFormatException e) {
                        // silent
                    }
                    count++;
                    downloadLog.setProperty(uri,
                            Integer.toString(count));
                }
                try {
                    downloadLog
                            .store(new FileWriter(logFile), "");
                } catch (IOException e) {
                }
            }
        });
        filterChain.doFilter(request, response);
    }
}

The filter's init method creates a downloadLog.txt file in the application directory if one does not yet exist.

        String appPath = filterConfig.getServletContext()
                .getRealPath("/");
        logFile = new File(appPath, "downloadLog.txt");
        if (!logFile.exists()) {
            try {
                logFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

It then creates a Properties object and loads the file:

        downloadLog = new Properties();
        try {
            downloadLog.load(new FileReader(logFile));
        } catch (IOException e) {
            e.printStackTrace();
        }

Note that the filter class has an ExecutorService (a subclass of Executor) as a class-level object reference.

    ExecutorService executorService = Executors
            .newSingleThreadExecutor();

The filter shuts down the ExecutorService when the filter is about to be destroyed.

    public void destroy() {
        executorService.shutdown();
    }

The doFilter method does the bulk of the job. It takes the URI of each request, calls the execute method of the ExecutorService, and calls FilterChain.doFilter(). The task that is passed to the execute method is easy to understand. It basically uses the URI as a property key and fetches the property value from the Properties object, increments it by one, and flushes the value back to the underlying log file.

            @Override
            public void run() {
                String property = downloadLog.getProperty(uri);
                if (property == null) {
                    downloadLog.setProperty(uri, "1");
                } else {
                    int count = 0;
                    try {
                        count = Integer.parseInt(property);
                    } catch (NumberFormatException e) {
                        // silent
                    }
                    count++;
                    downloadLog.setProperty(uri,
                            Integer.toString(count));
                }
                try {
                    downloadLog
                            .store(new FileWriter(logFile), "");
                } catch (IOException e) {
                }
            }

The filter works on any resource, but you can easily limit it to, say, PDF or AVI files only if you wish.

Filter Order

If you have multiple filters applied to the same resource and the order of invocation is important, you have to use the deployment descriptor to manage which filter should be invoked first. For example, if Filter1 must be invoked before Filter2, the declaration of Filter1 should appear before the declaration of Filter2 in the deployment descriptor.

<filter>
    <filter-name>Filter1</filter-name>
    <filter-class>
        the fully-qualified name of the filter class
    </filter-class>
</filter>
<filter>
    <filter-name>Filter2</filter-name>
    <filter-class>
        the fully-qualified name of the filter class
    </filter-class>
</filter>

It is not possible to manage filter invocation order without the deployment descriptor. See Chapter 16, “Deployment,” for more information about the deployment descriptor.

Summary

In this chapter you learned about the Filter API that includes the Filter interface, the FilterConfig interface and the FilterChain interface. You also learned how to write a filter by implementing the Filter interface and annotate the class using @WebFilter and registering it in the deployment descriptor.

There is only one instance for each type of filter. Therefore, thread safety may be an issue if you need to keep and change state in your filter class. The last filter example shows how to deal with this issue.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.240.21