# Class ExpiresFilter

java.lang.Object
org.apache.catalina.filters.FilterBase
org.apache.catalina.filters.ExpiresFilter

All Implemented Interfaces:
  `Filter`

public class ExpiresFilter extends FilterBase

ExpiresFilter is a Java Servlet API port of Apache mod_expires to add '`Expires`' and '`Cache-Control: max-age=`' headers to HTTP response according to its '`Content-Type`'.

Following documentation is inspired by mod_expires

## Summary

This filter controls the setting of the `Expires` HTTP header and the `max-age` directive of the `Cache-Control` HTTP header in server responses. The expiration date can set to be relative to either the time the source file was last modified, or to the time of the client access.

These HTTP headers are an instruction to the client about the document's validity and persistence. If cached, the document may be fetched from the cache rather than from the source until this time has passed. After that, the cache copy is considered "expired" and invalid, and a new copy must be obtained from the source.

To modify `Cache-Control` directives other than `max-age` (see RFC 2616 section 14.9), you can use other servlet filters or Apache Httpd mod_headers module.

## Filter Configuration

### Basic configuration to add '`Expires`' and '`Cache-Control: max-age=`' headers to images, CSS and JavaScript

    <web-app ...>
       ...
       <filter>
          <filter-name>ExpiresFilter</filter-name>
          <filter-class>org.apache.catalina.filters.ExpiresFilter</filter-class>
          <init-param>
             <param-name>ExpiresByType image</param-name>
             <param-value>access plus 10 minutes</param-value>
          </init-param>
          <init-param>
             <param-name>ExpiresByType text/css</param-name>
             <param-value>access plus 10 minutes</param-value>
          </init-param>
          <init-param>
             <param-name>ExpiresByType text/javascript</param-name>
             <param-value>access plus 10 minutes</param-value>
          </init-param>
       </filter>
       ...
       <filter-mapping>
          <filter-name>ExpiresFilter</filter-name>
          <url-pattern>/*</url-pattern>
          <dispatcher>REQUEST</dispatcher>
       </filter-mapping>
       ...
    </web-app>

### Configuration Parameters

#### `ExpiresByType <content-type>`

This directive defines the value of the `Expires` header and the `max-age` directive of the `Cache-Control` header generated for documents of the specified type (e.g., `text/html`). The second argument sets the number of seconds that will be added to a base time to construct the expiration date. The `Cache-Control: max-age` is calculated by subtracting the request time from the expiration date and expressing the result in seconds.

The base time is either the last modification time of the file, or the time of the client's access to the document. Which should be used is specified by the `<code>` field; `M` means that the file's last modification time should be used as the base time, and `A` means the client's access time should be used. The duration is expressed in seconds. `A2592000` stands for `access plus 30 days` in alternate syntax.

The difference in effect is subtle. If `M` (`modification` in alternate syntax) is used, all current copies of the document in all caches will expire at the same time, which can be good for something like a weekly notice that's always found at the same URL. If `A` ( `access` or `now` in alternate syntax) is used, the date of expiration is different for each client; this can be good for image files that don't change very often, particularly for a set of related documents that all refer to the same images (i.e., the images will be accessed repeatedly within a relatively short timespan).

Example:

    <init-param>
       <param-name>ExpiresByType text/html</param-name>
       <param-value>access plus 1 month 15 days 2 hours</param-value>
    </init-param>

    <init-param>
       <!-- 2592000 seconds = 30 days -->
       <param-name>ExpiresByType image/gif</param-name>
       <param-value>A2592000</param-value>
    </init-param>

Note that this directive only has effect if `ExpiresActive On` has been specified. It overrides, for the specified MIME type only, any expiration date set by the `ExpiresDefault` directive.

You can also specify the expiration time calculation using an alternate syntax, described earlier in this document.

#### `ExpiresExcludedResponseStatusCodes`

This directive defines the HTTP response status codes for which the `ExpiresFilter` will not generate expiration headers. By default, the `304` status code ("`Not modified`") is skipped. The value is a comma separated list of HTTP status codes.

This directive is useful to ease usage of `ExpiresDefault` directive. Indeed, the behavior of `304 Not modified` (which does specify a `Content-Type` header) combined with `Expires` and `Cache-Control:max-age=` headers can be unnecessarily tricky to understand.

Configuration sample :

    <init-param>
       <param-name>ExpiresExcludedResponseStatusCodes</param-name>
       <param-value>302, 500, 503</param-value>
    </init-param>

#### ExpiresDefault

This directive sets the default algorithm for calculating the expiration time for all documents in the affected realm. It can be overridden on a type-by-type basis by the `ExpiresByType` directive. See the description of that directive for details about the syntax of the argument, and the "alternate syntax" description as well.

## Alternate Syntax

The `ExpiresDefault` and `ExpiresByType` directives can also be defined in a more readable syntax of the form:

    <init-param>
       <param-name>ExpiresDefault</param-name>
       <param-value><base> [plus] (<num> <type>)*</param-value>
    </init-param>

    <init-param>
       <param-name>ExpiresByType type/encoding</param-name>
       <param-value><base> [plus] (<num> <type>)*</param-value>
    </init-param>

where `<base>` is one of:

- `access`
- `now` (equivalent to '`access`')
- `modification`

The `plus` keyword is optional. `<num>` should be an integer value (acceptable to `Integer.parseInt()`), and `<type>` is one of:

- `years`
- `months`
- `weeks`
- `days`
- `hours`
- `minutes`
- `seconds`

For example, any of the following directives can be used to make documents expire 1 month after being accessed, by default:

    <init-param>
       <param-name>ExpiresDefault</param-name>
       <param-value>access plus 1 month</param-value>
    </init-param>

    <init-param>
       <param-name>ExpiresDefault</param-name>
       <param-value>access plus 4 weeks</param-value>
    </init-param>

    <init-param>
       <param-name>ExpiresDefault</param-name>
       <param-value>access plus 30 days</param-value>
    </init-param>

The expiry time can be fine-tuned by adding several ' `<num> <type>`' clauses:

    <init-param>
       <param-name>ExpiresByType text/html</param-name>
       <param-value>access plus 1 month 15 days 2 hours</param-value>
    </init-param>

    <init-param>
       <param-name>ExpiresByType image/gif</param-name>
       <param-value>modification plus 5 hours 3 minutes</param-value>
    </init-param>

Note that if you use a modification date based setting, the `Expires` header will not be added to content that does not come from a file on disk. This is due to the fact that there is no modification time for such content.

## Expiration headers generation eligibility

A response is eligible to be enriched by `ExpiresFilter` if :

1. no expiration header is defined (`Expires` header or the `max-age` directive of the `Cache-Control` header),
2. the response status code is not excluded by the directive `ExpiresExcludedResponseStatusCodes`,
3. the `Content-Type` of the response matches one of the types defined the in `ExpiresByType` directives or the `ExpiresDefault` directive is defined.

Note :

- If `Cache-Control` header contains other directives than `max-age`, they are concatenated with the `max-age` directive that is added by the `ExpiresFilter`.

## Expiration configuration selection

The expiration configuration if elected according to the following algorithm:

1. `ExpiresByType` matching the exact content-type returned by `HttpServletResponse.getContentType()` possibly including the charset (e.g. '`text/xml;charset=UTF-8`'),
2. `ExpiresByType` matching the content-type without the charset if `HttpServletResponse.getContentType()` contains a charset (e.g. ' `text/xml;charset=UTF-8`' -> '`text/xml`'),
3. `ExpiresByType` matching the major type (e.g. substring before '`/`') of `HttpServletResponse.getContentType()` (e.g. '`text/xml;charset=UTF-8`' -> '`text` '),
4. `ExpiresDefault`

## Implementation Details

### When to write the expiration headers ?

The `ExpiresFilter` traps the 'on before write response body' event to decide whether it should generate expiration headers or not.

To trap the 'before write response body' event, the `ExpiresFilter` wraps the HTTP servlet response's writer and outputStream to intercept calls to the methods `write()`, `print()`, `close()` and `flush()`. For empty response body (e.g. empty files), the `write()`, `print()`, `close()` and `flush()` methods are not called; to handle this case, the `ExpiresFilter`, at the end of its `doFilter()` method, manually triggers the `onBeforeWriteResponseBody()` method.

### Configuration syntax

The `ExpiresFilter` supports the same configuration syntax as Apache Httpd mod_expires.

A challenge has been to choose the name of the `<param-name>` associated with `ExpiresByType` in the `<filter>` declaration. Indeed, Several `ExpiresByType` directives can be declared when `web.xml` syntax does not allow to declare several `<init-param>` with the same name.

The workaround has been to declare the content type in the `<param-name>` rather than in the `<param-value>`.

### Designed for extension : the open/close principle

The `ExpiresFilter` has been designed for extension following the open/close principle.

Key methods to override for extension are :

- `isEligibleToExpirationHeaderGeneration(HttpServletRequest, XHttpServletResponse)`
- `getExpirationDate(HttpServletRequest, XHttpServletResponse)`

## Troubleshooting

To troubleshoot, enable logging on the `org.apache.catalina.filters.ExpiresFilter`.

Extract of logging.properties

`org.apache.catalina.filters.ExpiresFilter.level = FINE`

Sample of initialization log message :

`Mar 26, 2010 2:01:41 PM org.apache.catalina.filters.ExpiresFilter init FINE: Filter initialized with configuration ExpiresFilter[ excludedResponseStatusCode=[304], default=null, byType={ image=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], text/css=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]], text/javascript=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]]}]`

Sample of per-request log message where `ExpiresFilter` adds an expiration date

`Mar 26, 2010 2:09:47 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody FINE: Request "/tomcat.gif" with response status "200" content-type "image/gif", set expiration date 3/26/10 2:19 PM`

Sample of per-request log message where `ExpiresFilter` does not add an expiration date

`Mar 26, 2010 2:10:27 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody FINE: Request "/docs/config/manager.html" with response status "200" content-type "text/html", no expiration configured`

## Nested Class Summary

Nested Classes
Modifier and Type
Class
Description
`static final record`
`ExpiresFilter.Duration`
Duration composed of an `ExpiresFilter.Duration.amount` and a `ExpiresFilter.Duration.unit`
`static enum`
`ExpiresFilter.DurationUnit`
Duration unit
`static final record`
`ExpiresFilter.ExpiresConfiguration`
Main piece of configuration of the filter.
`static enum`
`ExpiresFilter.StartingPoint`
Expiration configuration starting point.
`class`
`ExpiresFilter.XHttpServletResponse`
Wrapping extension of the `HttpServletResponse` to wrap the "Start Write Response Body" event.
`class`
`ExpiresFilter.XPrintWriter`
Wrapping extension of `PrintWriter` to trap the "Start Write Response Body" event.
`class`
`ExpiresFilter.XServletOutputStream`
Wrapping extension of `ServletOutputStream` to trap the "Start Write Response Body" event.

## Field Summary

### Fields inherited from class FilterBase

`sm`

## Constructor Summary

Constructors
Constructor
Description
`ExpiresFilter()`
Creates a new ExpiresFilter instance.

## Method Summary

Modifier and Type
Method
Description
`protected static int[]`
`commaDelimitedListToIntArray(String commaDelimitedInts)`
Convert a comma-delimited list of numbers into an `int[]`.
`protected static boolean`
`contains(String str, String searchStr)`
Checks whether the given string contains the given substring.
`void`
`doFilter(ServletRequest request, ServletResponse response, FilterChain chain)`
The `doFilter` method of the Filter is called by the container each time a request/response pair is passed through the chain due to a client request for a resource at the end of the chain.
`ExpiresFilter.ExpiresConfiguration`
`getDefaultExpiresConfiguration()`
Returns the default expiration configuration.
`String`
`getExcludedResponseStatusCodes()`
Returns the excluded response status codes as a comma-delimited string.
`int[]`
`getExcludedResponseStatusCodesAsInts()`
Returns the excluded response status codes as an integer array.
`protected Date`
`getExpirationDate(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)`
Returns the expiration date of the given `ExpiresFilter.XHttpServletResponse` or `null` if no expiration date has been configured for the declared content type.
`protected Date`
`getExpirationDate(ExpiresFilter.ExpiresConfiguration configuration, ExpiresFilter.XHttpServletResponse response)`
Returns the expiration date of the given `ExpiresFilter.ExpiresConfiguration`, `HttpServletRequest` and `ExpiresFilter.XHttpServletResponse`.
`Map<String, ExpiresFilter.ExpiresConfiguration>`
`getExpiresConfigurationByContentType()`
Returns the map of expiration configurations by content type.
`protected Log`
`getLogger()`
Returns the logger for this filter.
`void`
`init(FilterConfig filterConfig)`
Iterates over the configuration parameters and either logs a warning, or throws an exception for any parameter that does not have a matching setter in this filter.
`protected static String`
`intsToCommaDelimitedString(int[] ints)`
Convert an array of ints into a comma-delimited string
`protected boolean`
`isEligibleToExpirationHeaderGeneration(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)`
`protected` for extension.
`protected static boolean`
`isEmpty(String str)`
Checks if the given string is null or empty.
`protected static boolean`
`isNotEmpty(String str)`
Checks if the given string is not null and not empty.
`void`
`onBeforeWriteResponseBody(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)`
If no expiration header has been set by the servlet and an expiration has been defined in the `ExpiresFilter` configuration, sets the '`Expires`' header and the attribute '`max-age`' of the '`Cache-Control`' header.
`protected ExpiresFilter.ExpiresConfiguration`
`parseExpiresConfiguration(String inputLine)`
Parse configuration lines like '`access plus 1 month 15 days 2 hours`' or '`modification 1 day 2 hours 5 seconds`'
`void`
`setDefaultExpiresConfiguration(ExpiresFilter.ExpiresConfiguration defaultExpiresConfiguration)`
Sets the default expiration configuration.
`void`
`setExcludedResponseStatusCodes(int[] excludedResponseStatusCodes)`
Sets the excluded response status codes.
`void`
`setExpiresConfigurationByContentType(Map<String, ExpiresFilter.ExpiresConfiguration> expiresConfigurationByContentType)`
Sets the expiration configurations by content type.
`protected static boolean`
`startsWithIgnoreCase(String string, String prefix)`
Checks if the given string starts with the given prefix, ignoring case.
`protected static String`
`substringBefore(String str, String separator)`
Returns the subset of the given string that is before the first occurrence of the given separator.
`String`
`toString()`

### Methods inherited from class FilterBase

`isConfigProblemFatal`

### Methods inherited from class Object

`clone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait`

### Methods inherited from interface Filter

`destroy`

## Constructor Details

### ExpiresFilter

public ExpiresFilter()
Creates a new ExpiresFilter instance.

## Method Details

### commaDelimitedListToIntArray

protected static int[] commaDelimitedListToIntArray(String commaDelimitedInts)
Convert a comma-delimited list of numbers into an `int[]`.

Parameters:
  `commaDelimitedInts` - can be `null`
Returns:
  never `null` array

### contains

protected static boolean contains(String str, String searchStr)
Checks whether the given string contains the given substring.

Parameters:
  `str` - String that will be searched
  `searchStr` - The substring to search
Returns:
  `true` if the given `str` contains the given `searchStr`

### intsToCommaDelimitedString

protected static String intsToCommaDelimitedString(int[] ints)
Convert an array of ints into a comma-delimited string

Parameters:
  `ints` - The int array
Returns:
  a comma separated string

### isEmpty

protected static boolean isEmpty(String str)
Checks if the given string is null or empty.

Parameters:
  `str` - The String to check
Returns:
  `true` if the given `str` is `null` or has a zero characters length

### isNotEmpty

protected static boolean isNotEmpty(String str)
Checks if the given string is not null and not empty.

Parameters:
  `str` - The String to check
Returns:
  `true` if the given `str` has at least one character (can be a whitespace)

### startsWithIgnoreCase

protected static boolean startsWithIgnoreCase(String string, String prefix)
Checks if the given string starts with the given prefix, ignoring case.

Parameters:
  `string` - can be `null`
  `prefix` - can be `null`
Returns:
  `true` if the given `string` starts with the given `prefix` ignoring case

### substringBefore

protected static String substringBefore(String str, String separator)
Returns the subset of the given string that is before the first occurrence of the given separator. Returns `null` if the given `str` or the given `separator` is null. Returns an empty string if the `separator` is empty.

Parameters:
  `str` - can be `null`
  `separator` - can be `null`
Returns:
  the subset before the separator, or `null` if either argument is null

### doFilter

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
Description copied from interface: `jakarta.servlet.Filter`
The `doFilter` method of the Filter is called by the container each time a request/response pair is passed through the chain due to a client request for a resource at the end of the chain. The FilterChain passed in to this method allows the Filter to pass on the request and response to the next entity in the chain.

A typical implementation of this method would follow the following pattern:-
1. Examine the request
2. Optionally wrap the request object with a custom implementation to filter content or headers for input filtering
3. Optionally wrap the response object with a custom implementation to filter content or headers for output filtering
4. a) Either invoke the next entity in the chain using the FilterChain object (`chain.doFilter()`),
4. b) or not pass on the request/response pair to the next entity in the filter chain to block the request processing
5. Directly set headers on the response after invocation of the next entity in the filter chain.

Parameters:
  `request` - The request to process
  `response` - The response associated with the request
  `chain` - Provides access to the next filter in the chain for this filter to pass the request and response to for further processing
Throws:
  `IOException` - if an I/O error occurs during this filter's processing of the request
  `ServletException` - if the processing fails for any other reason

### getDefaultExpiresConfiguration

public ExpiresFilter.ExpiresConfiguration getDefaultExpiresConfiguration()
Returns the default expiration configuration.

Returns:
  the default expiration configuration, or `null` if none

### getExcludedResponseStatusCodes

public String getExcludedResponseStatusCodes()
Returns the excluded response status codes as a comma-delimited string.

Returns:
  the excluded status codes

### getExcludedResponseStatusCodesAsInts

public int[] getExcludedResponseStatusCodesAsInts()
Returns the excluded response status codes as an integer array.

Returns:
  the excluded status codes as integers

### getExpirationDate

protected Date getExpirationDate(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)
Returns the expiration date of the given `ExpiresFilter.XHttpServletResponse` or `null` if no expiration date has been configured for the declared content type.

`protected` for extension.

Parameters:
  `request` - The HTTP request
  `response` - The wrapped HTTP response
Returns:
  the expiration date
See Also:
  - `ServletResponse.getContentType()`

### getExpirationDate

protected Date getExpirationDate(ExpiresFilter.ExpiresConfiguration configuration, ExpiresFilter.XHttpServletResponse response)

Returns the expiration date of the given `ExpiresFilter.ExpiresConfiguration`, `HttpServletRequest` and `ExpiresFilter.XHttpServletResponse`.

`protected` for extension.

Parameters:
  `configuration` - The parsed expires
  `response` - The Servlet response
Returns:
  the expiration date

### getExpiresConfigurationByContentType

public Map<String, ExpiresFilter.ExpiresConfiguration> getExpiresConfigurationByContentType()
Returns the map of expiration configurations by content type.

Returns:
  the map of content type to expiration configuration

### getLogger

protected Log getLogger()
Description copied from class: `FilterBase`
Returns the logger for this filter.

Specified by:
  `getLogger` in class `FilterBase`
Returns:
  the logger

### init

public void init(FilterConfig filterConfig) throws ServletException
Description copied from class: `FilterBase`
Iterates over the configuration parameters and either logs a warning, or throws an exception for any parameter that does not have a matching setter in this filter.

Specified by:
  `init` in interface `Filter`
Overrides:
  `init` in class `FilterBase`
Parameters:
  `filterConfig` - The configuration information associated with the filter instance being initialised
Throws:
  `ServletException` - if `FilterBase.isConfigProblemFatal()` returns `true` and a configured parameter does not have a matching setter

### isEligibleToExpirationHeaderGeneration

protected boolean isEligibleToExpirationHeaderGeneration(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)

`protected` for extension.

Parameters:
  `request` - The Servlet request
  `response` - The Servlet response
Returns:
  `true` if an expire header may be added

### onBeforeWriteResponseBody

public void onBeforeWriteResponseBody(HttpServletRequest request, ExpiresFilter.XHttpServletResponse response)

If no expiration header has been set by the servlet and an expiration has been defined in the `ExpiresFilter` configuration, sets the '`Expires`' header and the attribute '`max-age`' of the '`Cache-Control`' header.

Must be called on the "Start Write Response Body" event.

Invocations to `Logger.debug(...)` are guarded by `Log.isDebugEnabled()` because `HttpServletRequest.getRequestURI()` and `ServletResponse.getContentType()` costs `String` objects instantiations (as of Tomcat 7).

Parameters:
  `request` - The Servlet request
  `response` - The Servlet response

### parseExpiresConfiguration

protected ExpiresFilter.ExpiresConfiguration parseExpiresConfiguration(String inputLine)
Parse configuration lines like '`access plus 1 month 15 days 2 hours`' or '`modification 1 day 2 hours 5 seconds`'

Parameters:
  `inputLine` - the input
Returns:
  the parsed expires

### setDefaultExpiresConfiguration

public void setDefaultExpiresConfiguration(ExpiresFilter.ExpiresConfiguration defaultExpiresConfiguration)
Sets the default expiration configuration.

Parameters:
  `defaultExpiresConfiguration` - The default expiration configuration

### setExcludedResponseStatusCodes

public void setExcludedResponseStatusCodes(int[] excludedResponseStatusCodes)
Sets the excluded response status codes.

Parameters:
  `excludedResponseStatusCodes` - The status codes to exclude

### setExpiresConfigurationByContentType

public void setExpiresConfigurationByContentType(Map<String, ExpiresFilter.ExpiresConfiguration> expiresConfigurationByContentType)
Sets the expiration configurations by content type.

Parameters:
  `expiresConfigurationByContentType` - The map of content type to expiration configuration

### toString

public String toString()

Overrides:
  `toString` in class `Object`