Start line:  
End line:  

Snippet Preview

Snippet HTML Code

Stack Overflow Questions
   /*
    * Copyright 2008-2009 the original author or authors.
    *
    * Licensed under the Apache License, Version 2.0 (the "License");
    * you may not use this file except in compliance with the License.
    * You may obtain a copy of the License at
    *
    *      http://www.apache.org/licenses/LICENSE-2.0
    *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.apache.catalina.filters;
  
  import static org.jboss.web.CatalinaMessages.MESSAGES;
  
  import java.util.Arrays;
  import java.util.Date;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  
  

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&#x27;s access to the document. Which should be used is specified by the <code> field; M means that the file&#x27;s last modification time should be used as the base time, and A means the client&#x27;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&#x27;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&#x27;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 &#x27;access&#x27;)
  • 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 &#x27; <num> <type>&#x27; 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. &#x27;text/xml;charset=UTF-8&#x27;),
  2. ExpiresByType matching the content-type without the charset if HttpServletResponse.getContentType() contains a charset (e.g. &#x27; text/xml;charset=UTF-8&#x27; -> &#x27;text/xml&#x27;),
  3. ExpiresByType matching the major type (e.g. substring before &#x27;/&#x27;) of HttpServletResponse.getContentType() (e.g. &#x27;text/xml;charset=UTF-8&#x27; -> &#x27;text &#x27;),
  4. ExpiresDefault

Implementation Details

When to write the expiration headers ?

The ExpiresFilter traps the &#x27;on before write response body&#x27; event to decide whether it should generate expiration headers or not.

To trap the &#x27;before write response body&#x27; event, the ExpiresFilter wraps the http servlet response&#x27;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 :

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
 
 
 public class ExpiresFilter extends FilterBase {

    
Duration composed of an amount and a unit
 
     protected static class Duration {
 
         public static Duration minutes(int amount) {
             return new Duration(amount.);
         }
 
         public static Duration seconds(int amount) {
             return new Duration(amount.);
         }
 
         final protected int amount;
 
         final protected DurationUnit unit;
 
         public Duration(int amountDurationUnit unit) {
             super();
             this. = amount;
             this. = unit;
         }
 
         public int getAmount() {
             return ;
         }
 
         public DurationUnit getUnit() {
             return ;
         }
 
         @Override
         public String toString() {
             return  + " " + ;
         }
     }

    
Duration unit
 
     protected enum DurationUnit {
         DAY(.), HOUR(.), MINUTE(.), MONTH(
                 .), SECOND(.), WEEK(
                 .), YEAR(.);
         private final int calendardField;
 
         private DurationUnit(int calendardField) {
             this. = calendardField;
         }
 
         public int getCalendardField() {
             return ;
         }
 
     }

    

Main piece of configuration of the filter.

Can be expressed like 'access plus 1 month 15 days 2 hours'.

 
     protected static class ExpiresConfiguration {
        
List of duration elements.
 
         private List<Durationdurations;

        
Starting point of the elaspse to set in the response.
 
         private StartingPoint startingPoint;
 
         public ExpiresConfiguration(StartingPoint startingPoint,
                 Duration... durations) {
             this(startingPoint, Arrays.asList(durations));
         }
 
         public ExpiresConfiguration(StartingPoint startingPoint,
                 List<Durationdurations) {
             super();
             this. = startingPoint;
             this. = durations;
         }
 
         public List<DurationgetDurations() {
             return ;
         }
 
         public StartingPoint getStartingPoint() {
             return ;
         }
 
         @Override
         public String toString() {
             return "ExpiresConfiguration[startingPoint=" +  +
                     ", duration=" +  + "]";
         }
     }

    
Expiration configuration starting point. Either the time the html-page/servlet-response was served (ACCESS_TIME) or the last time the html-page/servlet-response was modified ( LAST_MODIFICATION_TIME).
 
     protected enum StartingPoint {
         ACCESS_TIME, LAST_MODIFICATION_TIME
     }

    

Wrapping extension of the javax.servlet.http.HttpServletResponse to yrap the "Start Write Response Body" event.

For performance optimization : this extended response holds the lastModifiedHeader and cacheControlHeader values access to the slow javax.servlet.http.HttpServletResponseWrapper.getHeader(java.lang.String) and to spare the string to date to long conversion.

 
     public class XHttpServletResponse extends HttpServletResponseWrapper {

        
Value of the Cache-Control/tt> http response header if it has been set.
 
         private String cacheControlHeader;

        
Value of the Last-Modified http response header if it has been set.
 
         private long lastModifiedHeader;
 
         private boolean lastModifiedHeaderSet;
 
         private PrintWriter printWriter;
 
         private HttpServletRequest request;
 
         private ServletOutputStream servletOutputStream;

        
Indicates whether calls to write methods (write(...), print(...), etc) of the response body have been called or not.
 
         private boolean writeResponseBodyStarted;
 
         public XHttpServletResponse(HttpServletRequest request,
                 HttpServletResponse response) {
             super(response);
             this. = request;
         }
 
         @Override
         public void addDateHeader(String namelong date) {
             super.addDateHeader(namedate);
             if (!) {
                 this. = date;
                 this. = true;
             }
         }
 
         @Override
         public void addHeader(String nameString value) {
             super.addHeader(namevalue);
             if (.equalsIgnoreCase(name) &&
                      == null) {
                  = value;
             }
         }
 
         public String getCacheControlHeader() {
             return ;
         }
 
         public long getLastModifiedHeader() {
             return ;
         }
 
         @Override
         public ServletOutputStream getOutputStream() throws IOException {
             if ( == null) {
                  = new XServletOutputStream(
                         super.getOutputStream(), this);
             }
             return ;
         }
 
         @Override
         public PrintWriter getWriter() throws IOException {
             if ( == null) {
                  = new XPrintWriter(super.getWriter(), this);
             }
             return ;
         }
 
         public boolean isLastModifiedHeaderSet() {
             return ;
         }
 
         public boolean isWriteResponseBodyStarted() {
             return ;
         }
 
         @Override
         public void reset() {
             super.reset();
             this. = 0;
             this. = false;
             this. = null;
         }
 
         @Override
         public void setDateHeader(String namelong date) {
             super.setDateHeader(namedate);
             if (.equalsIgnoreCase(name)) {
                 this. = date;
                 this. = true;
             }
         }
 
         @Override
         public void setHeader(String nameString value) {
             super.setHeader(namevalue);
             if (.equalsIgnoreCase(name)) {
                 this. = value;
             }
         }
 
         public void setWriteResponseBodyStarted(boolean writeResponseBodyStarted) {
             this. = writeResponseBodyStarted;
         }
     }

    
Wrapping extension of java.io.PrintWriter to trap the "Start Write Response Body" event.
 
     public class XPrintWriter extends PrintWriter {
         private PrintWriter out;
 
         private HttpServletRequest request;
 
         private XHttpServletResponse response;
 
         public XPrintWriter(PrintWriter outHttpServletRequest request,
                 XHttpServletResponse response) {
             super(out);
             this. = out;
             this. = request;
             this. = response;
         }
 
         @Override
         public PrintWriter append(char c) {
             fireBeforeWriteResponseBodyEvent();
             return .append(c);
         }
 
         @Override
         public PrintWriter append(CharSequence csq) {
             fireBeforeWriteResponseBodyEvent();
             return .append(csq);
         }
 
         @Override
         public PrintWriter append(CharSequence csqint startint end) {
             fireBeforeWriteResponseBodyEvent();
             return .append(csqstartend);
         }
 
         @Override
         public void close() {
             fireBeforeWriteResponseBodyEvent();
             .close();
         }
 
         private void fireBeforeWriteResponseBodyEvent() {
             if (!this..isWriteResponseBodyStarted()) {
                 this..setWriteResponseBodyStarted(true);
                 onBeforeWriteResponseBody();
             }
         }
 
         @Override
         public void flush() {
             fireBeforeWriteResponseBodyEvent();
             .flush();
         }
 
         @Override
         public void print(boolean b) {
             fireBeforeWriteResponseBodyEvent();
             .print(b);
         }
 
         @Override
         public void print(char c) {
             fireBeforeWriteResponseBodyEvent();
             .print(c);
         }
 
         @Override
         public void print(char[] s) {
             fireBeforeWriteResponseBodyEvent();
             .print(s);
         }
 
         @Override
         public void print(double d) {
             fireBeforeWriteResponseBodyEvent();
             .print(d);
         }
 
         @Override
         public void print(float f) {
             fireBeforeWriteResponseBodyEvent();
             .print(f);
         }
 
         @Override
         public void print(int i) {
             fireBeforeWriteResponseBodyEvent();
             .print(i);
         }
 
         @Override
         public void print(long l) {
             fireBeforeWriteResponseBodyEvent();
             .print(l);
         }
 
         @Override
         public void print(Object obj) {
             fireBeforeWriteResponseBodyEvent();
             .print(obj);
         }
 
         @Override
         public void print(String s) {
             fireBeforeWriteResponseBodyEvent();
             .print(s);
         }
 
         @Override
         public PrintWriter printf(Locale lString formatObject... args) {
             fireBeforeWriteResponseBodyEvent();
             return .printf(lformatargs);
         }
 
         @Override
         public PrintWriter printf(String formatObject... args) {
             fireBeforeWriteResponseBodyEvent();
             return .printf(formatargs);
         }
 
         @Override
         public void println() {
             fireBeforeWriteResponseBodyEvent();
             .println();
         }
 
         @Override
         public void println(boolean x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(char x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(char[] x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(double x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(float x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(int x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(long x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(Object x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void println(String x) {
             fireBeforeWriteResponseBodyEvent();
             .println(x);
         }
 
         @Override
         public void write(char[] buf) {
             fireBeforeWriteResponseBodyEvent();
             .write(buf);
         }
 
         @Override
         public void write(char[] bufint offint len) {
             fireBeforeWriteResponseBodyEvent();
             .write(bufofflen);
         }
 
         @Override
         public void write(int c) {
             fireBeforeWriteResponseBodyEvent();
             .write(c);
         }
 
         @Override
         public void write(String s) {
             fireBeforeWriteResponseBodyEvent();
             .write(s);
         }
 
         @Override
         public void write(String sint offint len) {
             fireBeforeWriteResponseBodyEvent();
             .write(sofflen);
         }
 
     }

    
Wrapping extension of javax.servlet.ServletOutputStream to trap the "Start Write Response Body" event.
 
     public class XServletOutputStream extends ServletOutputStream {
 
         private HttpServletRequest request;
 
         private XHttpServletResponse response;
 
         private ServletOutputStream servletOutputStream;
 
         public XServletOutputStream(ServletOutputStream servletOutputStream,
                 HttpServletRequest requestXHttpServletResponse response) {
             super();
             this. = servletOutputStream;
             this. = response;
             this. = request;
         }
 
         @Override
         public void close() throws IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .close();
         }
 
         private void fireOnBeforeWriteResponseBodyEvent() {
             if (!this..isWriteResponseBodyStarted()) {
                 this..setWriteResponseBodyStarted(true);
                 onBeforeWriteResponseBody();
             }
         }
 
         @Override
         public void flush() throws IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .flush();
         }
 
         @Override
         public void print(boolean bthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(b);
         }
 
         @Override
         public void print(char cthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(c);
         }
 
         @Override
         public void print(double dthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(d);
         }
 
         @Override
         public void print(float fthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(f);
         }
 
         @Override
         public void print(int ithrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(i);
         }
 
         @Override
         public void print(long lthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(l);
         }
 
         @Override
         public void print(String sthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .print(s);
         }
 
         @Override
         public void println() throws IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println();
         }
 
         @Override
         public void println(boolean bthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(b);
         }
 
         @Override
         public void println(char cthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(c);
         }
 
         @Override
         public void println(double dthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(d);
         }
 
         @Override
         public void println(float fthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(f);
         }
 
         @Override
         public void println(int ithrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(i);
         }
 
         @Override
         public void println(long lthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(l);
         }
 
         @Override
         public void println(String sthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .println(s);
         }
 
         @Override
         public void write(byte[] bthrows IOException {
             fireOnBeforeWriteResponseBodyEvent();
             .write(b);
         }
 
         @Override
        public void write(byte[] bint offint lenthrows IOException {
            fireOnBeforeWriteResponseBodyEvent();
            .write(bofflen);
        }
        @Override
        public void write(int bthrows IOException {
            fireOnBeforeWriteResponseBodyEvent();
            .write(b);
        }
    }

    
java.util.regex.Pattern for a comma delimited string that support whitespace characters
    private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*");
    private static final String HEADER_CACHE_CONTROL = "Cache-Control";
    private static final String HEADER_EXPIRES = "Expires";
    private static final String HEADER_LAST_MODIFIED = "Last-Modified";
    private static final String PARAMETER_EXPIRES_BY_TYPE = "ExpiresByType";
    private static final String PARAMETER_EXPIRES_DEFAULT = "ExpiresDefault";
    private static final String PARAMETER_EXPIRES_EXCLUDED_RESPONSE_STATUS_CODES = "ExpiresExcludedResponseStatusCodes";

    
Convert a comma delimited list of numbers into an int[].

Parameters:
commaDelimitedInts can be null
Returns:
never null array
    protected static int[] commaDelimitedListToIntArray(
            String commaDelimitedInts) {
        String[] intsAsStrings = commaDelimitedListToStringArray(commaDelimitedInts);
        int[] ints = new int[intsAsStrings.length];
        for (int i = 0; i < intsAsStrings.lengthi++) {
            String intAsString = intsAsStrings[i];
            try {
                ints[i] = Integer.parseInt(intAsString);
            } catch (NumberFormatException e) {
                throw .invalidNumberInList(icommaDelimitedInts);
            }
        }
        return ints;
    }

    
Convert a given comma delimited list of strings into an array of String

Returns:
array of patterns (non null)
    protected static String[] commaDelimitedListToStringArray(
            String commaDelimitedStrings) {
        return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0]
                : .split(commaDelimitedStrings);
    }

    
Return true if the given str contains the given searchStr.
    protected static boolean contains(String strString searchStr) {
        if (str == null || searchStr == null) {
            return false;
        }
        return str.indexOf(searchStr) >= 0;
    }

    
Convert an array of ints into a comma delimited string
    protected static String intsToCommaDelimitedString(int[] ints) {
        if (ints == null) {
            return "";
        }
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < ints.lengthi++) {
            result.append(ints[i]);
            if (i < (ints.length - 1)) {
                result.append(", ");
            }
        }
        return result.toString();
    }

    
Return true if the given str is null or has a zero characters length.
    protected static boolean isEmpty(String str) {
        return str == null || str.length() == 0;
    }

    
Return true if the given str has at least one character (can be a withespace).
    protected static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    
Return true if the given string starts with the given prefix ignoring case.

Parameters:
string can be null
prefix can be null
    protected static boolean startsWithIgnoreCase(String stringString prefix) {
        if (string == null || prefix == null) {
            return string == null && prefix == null;
        }
        if (prefix.length() > string.length()) {
            return false;
        }
        return string.regionMatches(true, 0, prefix, 0, prefix.length());
    }

    
Return the subset of the given str that is before the first occurence of the given separator. Return null if the given str or the given separator is null. Return and empty string if the separator is empty.

Parameters:
str can be null
separator can be null
Returns:
    protected static String substringBefore(String strString separator) {
        if (str == null || str.isEmpty() || separator == null) {
            return null;
        }
        if (separator.isEmpty()) {
            return "";
        }
        int separatorIndex = str.indexOf(separator);
        if (separatorIndex == -1) {
            return str;
        }
        return str.substring(0, separatorIndex);
    }

    
Default Expires configuration.
list of response status code for which the ExpiresFilter will not generate expiration headers.
    private int[] excludedResponseStatusCodes = new int[] { . };

    
Expires configuration by content type. Visible for test.
    public void doFilter(ServletRequest requestServletResponse response,
            FilterChain chainthrows IOExceptionServletException {
        if (request instanceof HttpServletRequest &&
                response instanceof HttpServletResponse) {
            HttpServletRequest httpRequest = (HttpServletRequestrequest;
            HttpServletResponse httpResponse = (HttpServletResponseresponse;
            if (response.isCommitted()) {
                ..expiresResponseAlreadyCommitted(httpRequest.getRequestURI());
                chain.doFilter(requestresponse);
            } else {
                XHttpServletResponse xResponse = new XHttpServletResponse(
                        httpRequesthttpResponse);
                chain.doFilter(requestxResponse);
                if (!xResponse.isWriteResponseBodyStarted()) {
                    // Empty response, manually trigger
                    // onBeforeWriteResponseBody()
                    onBeforeWriteResponseBody(httpRequestxResponse);
                }
            }
        } else {
            chain.doFilter(requestresponse);
        }
    }
        return ;
    }
    }
    public int[] getExcludedResponseStatusCodesAsInts() {
        return ;
    }

    

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.

    protected Date getExpirationDate(XHttpServletResponse response) {
        String contentType = response.getContentType();
        // lookup exact content-type match (e.g.
        // "text/html; charset=iso-8859-1")
        ExpiresConfiguration configuration = .get(contentType);
        if (configuration != null) {
            Date result = getExpirationDate(configurationresponse);
            return result;
        }
        if (contains(contentType";")) {
            // lookup content-type without charset match (e.g. "text/html")
            String contentTypeWithoutCharset = substringBefore(contentType";").trim();
            configuration = .get(contentTypeWithoutCharset);
            if (configuration != null) {
                Date result = getExpirationDate(configurationresponse);
                return result;
            }
        }
        if (contains(contentType"/")) {
            // lookup major type match (e.g. "text")
            String majorType = substringBefore(contentType"/");
            configuration = .get(majorType);
            if (configuration != null) {
                Date result = getExpirationDate(configurationresponse);
                return result;
            }
        }
        if ( != null) {
            Date result = getExpirationDate(,
                    response);
            return result;
        }
        return null;
    }

    

Returns the expiration date of the given ExpiresFilter.ExpiresConfiguration, javax.servlet.http.HttpServletRequest and ExpiresFilter.XHttpServletResponse.

protected for extension.

    protected Date getExpirationDate(ExpiresConfiguration configuration,
            XHttpServletResponse response) {
        Calendar calendar;
        switch (configuration.getStartingPoint()) {
        case :
            calendar = Calendar.getInstance();
            break;
        case :
            if (response.isLastModifiedHeaderSet()) {
                try {
                    long lastModified = response.getLastModifiedHeader();
                    calendar = Calendar.getInstance();
                    calendar.setTimeInMillis(lastModified);
                } catch (NumberFormatException e) {
                    // default to now
                    calendar = Calendar.getInstance();
                }
            } else {
                // Last-Modified header not found, use now
                calendar = Calendar.getInstance();
            }
            break;
        default:
            throw .expiresUnsupportedStartingPoint("" + configuration.getStartingPoint());
        }
        for (Duration duration : configuration.getDurations()) {
            calendar.add(duration.getUnit().getCalendardField(),
                    duration.getAmount());
        }
        return calendar.getTime();
    }
        return ;
    }
    @Override
    public void init(FilterConfig filterConfigthrows ServletException {
        for (Enumeration<Stringnames = filterConfig.getInitParameterNames(); names.hasMoreElements();) {
            String name = names.nextElement();
            String value = filterConfig.getInitParameter(name);
            try {
                if (name.startsWith()) {
                    String contentType = name.substring(
                            .length()).trim();
                    ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value);
                    this..put(contentType,
                            expiresConfiguration);
                } else if (name.equalsIgnoreCase()) {
                    ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value);
                    this. = expiresConfiguration;
                } else if (name.equalsIgnoreCase()) {
                    this. = commaDelimitedListToIntArray(value);
                } else {
                    ..expiresUnknownParameter(namevalue);
                }
            } catch (RuntimeException e) {
                throw new ServletException(.expiresExceptionProcessingParameter(namevalue), e);
            }
        }
    }

    

protected for extension.

    protected boolean isEligibleToExpirationHeaderGeneration(
            HttpServletRequest requestXHttpServletResponse response) {
        boolean expirationHeaderHasBeenSet = response.containsHeader() ||
                contains(response.getCacheControlHeader(), "max-age");
        if (expirationHeaderHasBeenSet) {
            ..expiresHeaderAlreadyDefined(request.getRequestURI(), response.getStatus(), response.getContentType());
            return false;
        }
        for (int skippedStatusCode : this.) {
            if (response.getStatus() == skippedStatusCode) {
                ..expiresSkipStatusCode(request.getRequestURI(), response.getStatus(), response.getContentType());
                return false;
            }
        }
        return true;
    }

    

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 org.jboss.logging.Logger.isDebugEnabled() because javax.servlet.http.HttpServletRequest.getRequestURI() and javax.servlet.ServletResponse.getContentType() costs String objects instantiations (as of Tomcat 7).

    public void onBeforeWriteResponseBody(HttpServletRequest request,
            XHttpServletResponse response) {
        if (!isEligibleToExpirationHeaderGeneration(requestresponse)) {
            return;
        }
        Date expirationDate = getExpirationDate(response);
        if (expirationDate != null) {
            String maxAgeDirective = "max-age=" +
                    ((expirationDate.getTime() - System.currentTimeMillis()) / 1000);
            String cacheControlHeader = response.getCacheControlHeader();
            String newCacheControlHeader = (cacheControlHeader == null) ? maxAgeDirective
                    : cacheControlHeader + ", " + maxAgeDirective;
            response.setHeader(newCacheControlHeader);
            response.setDateHeader(expirationDate.getTime());
        }
    }

    
Parse configuration lines like ' access plus 1 month 15 days 2 hours' or ' modification 1 day 2 hours 5 seconds'

Parameters:
inputLine
    protected ExpiresConfiguration parseExpiresConfiguration(String inputLine) {
        String line = inputLine.trim();
        StringTokenizer tokenizer = new StringTokenizer(line" ");
        String currentToken;
        try {
            currentToken = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw .expiresStartingPointNotFound(line);
        }
        StartingPoint startingPoint;
        if ("access".equalsIgnoreCase(currentToken) ||
                "now".equalsIgnoreCase(currentToken)) {
            startingPoint = .;
        } else if ("modification".equalsIgnoreCase(currentToken)) {
            startingPoint = .;
        } else if (!tokenizer.hasMoreTokens() &&
                startsWithIgnoreCase(currentToken"a")) {
            startingPoint = .;
            // trick : convert duration configuration from old to new style
            tokenizer = new StringTokenizer(currentToken.substring(1) +
                    " seconds"" ");
        } else if (!tokenizer.hasMoreTokens() &&
                startsWithIgnoreCase(currentToken"m")) {
            startingPoint = .;
            // trick : convert duration configuration from old to new style
            tokenizer = new StringTokenizer(currentToken.substring(1) +
                    " seconds"" ");
        } else {
            throw .expiresInvalidStartingPoint(currentTokenline);
        }
        try {
            currentToken = tokenizer.nextToken();
        } catch (NoSuchElementException e) {
            throw .expiresDurationNotFound(line);
        }
        if ("plus".equalsIgnoreCase(currentToken)) {
            // skip
            try {
                currentToken = tokenizer.nextToken();
            } catch (NoSuchElementException e) {
                throw .expiresDurationNotFound(line);
            }
        }
        List<Durationdurations = new ArrayList<Duration>();
        while (currentToken != null) {
            int amount;
            try {
                amount = Integer.parseInt(currentToken);
            } catch (NumberFormatException e) {
                throw .expiresInvalidDuration(currentTokenline);
            }
            try {
                currentToken = tokenizer.nextToken();
            } catch (NoSuchElementException e) {
                throw .expiresDurationUnitNotFound(amountline);
            }
            DurationUnit durationUnit;
            if ("years".equalsIgnoreCase(currentToken)) {
                durationUnit = .;
            } else if ("month".equalsIgnoreCase(currentToken) ||
                    "months".equalsIgnoreCase(currentToken)) {
                durationUnit = .;
            } else if ("week".equalsIgnoreCase(currentToken) ||
                    "weeks".equalsIgnoreCase(currentToken)) {
                durationUnit = .;
            } else if ("day".equalsIgnoreCase(currentToken) ||
                    "days".equalsIgnoreCase(currentToken)) {
                durationUnit = .