/*
* Copyright 2017 OmniFaces
*
* 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.omnifaces.filter;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.omnifaces.util.Servlets.isFacesDevelopment;
import static org.omnifaces.util.Servlets.isFacesResourceRequest;
import static org.omnifaces.util.Servlets.setCacheHeaders;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.omnifaces.util.Servlets;
/**
* <p>
* This filter will control the cache-related headers of the response. Cache-related headers have a major impact on
* performance (network bandwidth and server load) and user experience (up to date content and non-expired views).
* <p>
* By default, when no initialization parameters are specified, the filter will instruct the client (generally, the
* webbrowser) to <strong>not</strong> cache the response. This is recommended on dynamic pages with stateful forms with
* a <code>javax.faces.ViewState</code> hidden field. If such a page were cached, and the enduser navigates to it by
* webbrowser's back button, and then re-submits it, then the enduser would face a
* <a href="http://stackoverflow.com/a/3642969/157882"><code>ViewExpiredException</code></a>.
* <p>
* However, on stateless resources, caching the response would be beneficial. Set the expire time to the same time as
* you'd like to use as refresh interval of the resource, which can be 10 seconds (to avoid F5-madness on resources
* which are subject to quick changes), but also minutes or even hours, days or weeks. For example, a list of links, a
* news page, a JS/CSS/image file, etc.
* <p>
* Any sane server and client adheres the following rules as to caching:
* <ul>
* <li>When the enduser performs page-to-page navigation, or when the enduser selects URL in address bar and presses
* enter key again, while the resource is cached, then the client will just load it from the cache without hitting the
* server in any way.
* <li>Or when the cache is expired, or when the enduser does a soft-refresh by pressing refresh button or F5 key, then:
* <ul>
* <li>When the <code>ETag</code> or <code>Last-Modified</code> header is present on cached resource, then the client
* will perform a so-called conditional GET request with <code>If-None-Match</code> or <code>If-Modified-Since</code>
* headers. If the server responds with HTTP status 304 ("not modified") along with the updated cache-related headers,
* then the client will keep the resource in cache and expand its expire time based on the headers. Note:
* <code>ETag</code> takes precedence over <code>Last-Modified</code> when both are present and consequently
* <code>If-None-Match</code> takes precedence over <code>If-Modified-Since</code> when both are present.
* <li>When those headers are <strong>not</strong> present, then the behavior is the same as during a hard-refresh.
* </ul>
* <li>Or when the resource is not cached, or when the enduser does a hard-refresh by pressing <code>Ctrl</code> key
* along with refresh button or F5, then the webbrowser will perform a fresh new request and purge any cached resource.
* </ul>
* <p>
* <strong>Important notice</strong>: this filter automatically skips JSF resources, such as the ones served by
* <code><h:outputScript></code>, <code><h:outputStylesheet></code>, <code>@ResourceDependency</code>, etc.
* Their cache-related headers are namely <a href="http://stackoverflow.com/q/15057932/157882">already</a> controlled
* by the <code>ResourceHandler</code> implementation. In Mojarra and MyFaces, the default expiration time is 1 week
* (604800000 milliseconds), which can be configured by a <code>web.xml</code> context parameter with the following name and
* a value in milliseconds, e.g. <code>3628800000</code> for 6 weeks:
* <ul>
* <li>Mojarra: <code>com.sun.faces.defaultResourceMaxAge</code>
* <li>MyFaces: <code>org.apache.myfaces.RESOURCE_MAX_TIME_EXPIRES</code>
* </ul>
* <p>
* It would not make sense to control their cache-related headers with this filter as they would be overridden anyway.
*
* <h3>Configuration</h3>
* <p>
* This filter supports the <code>expires</code> initialization parameter which must be a number between 0 and 999999999
* with optionally the 'w', 'd', 'h', 'm' or 's' suffix standing for respectively 'week', 'day', 'hour', 'minute' and
* 'second'. For example: '6w' is 6 weeks. The default suffix is 's'. So, when the suffix is omitted, it's treated as
* seconds. For example: '86400' is 86400 seconds, which is effectively equal to '86400s', '1440m', '24h' and '1d'.
* <p>
* Imagine that you've the following resources:
* <ul>
* <li>All <code>/forum/*</code> pages: cache 10 seconds.
* <li>All <code>*.pdf</code> and <code>*.zip</code> files: cache 2 days.
* <li>All other pages: no cache.
* </ul>
* <p>
* Then you can configure the filter as follows (filter name is fully free to your choice, but keep it sensible):
* <pre>
* <filter>
* <filter-name>noCache</filter-name>
* <filter-class>org.omnifaces.filter.CacheControlFilter</filter-class>
* </filter>
* <filter>
* <filter-name>cache10seconds</filter-name>
* <filter-class>org.omnifaces.filter.CacheControlFilter</filter-class>
* <init-param>
* <param-name>expires</param-name>
* <param-value>10s</param-value>
* </init-param>
* </filter>
* <filter>
* <filter-name>cache2days</filter-name>
* <filter-class>org.omnifaces.filter.CacheControlFilter</filter-class>
* <init-param>
* <param-name>expires</param-name>
* <param-value>2d</param-value>
* </init-param>
* </filter>
*
* <filter-mapping>
* <filter-name>noCache</filter-name>
* <url-pattern>/*</url-pattern>
* </filter-mapping>
* <filter-mapping>
* <filter-name>cache10seconds</filter-name>
* <url-pattern>/forum/*</url-pattern>
* </filter-mapping>
* <filter-mapping>
* <filter-name>cache2days</filter-name>
* <url-pattern>*.pdf</url-pattern>
* <url-pattern>*.zip</url-pattern>
* </filter-mapping>
* </pre>
* <p>
* Note: put the more specific URL patterns in the end of filter mappings. Due to the way how filters work, there's
* unfortunately no simple way to skip the filter on <code>/*</code> when e.g. <code>*.pdf</code> is matched. You can
* always map the no cache filter specifically to <code>FacesServlet</code> if you intend to disable caching on
* <strong>all</strong> JSF pages. Here's an example assuming that you've configured the <code>FacesServlet</code> with
* a servlet name of <code>facesServlet</code>:
* <pre>
* <filter-mapping>
* <filter-name>noCache</filter-name>
* <servlet-name>facesServlet</servlet-name>
* </filter-mapping>
* </pre>
*
* <h3>Actual headers</h3>
* <p>If the <code>expires</code> init param is set with a value which represents a time larger than 0 seconds, then the
* following headers will be set:
* <ul>
* <li><code>Cache-Control: public,max-age=[expiration time in seconds],must-revalidate</code></li>
* <li><code>Expires: [expiration date of now plus expiration time in seconds]</code></li>
* </ul>
* <p>If the <code>expires</code> init param is absent, or set with a value which represents a time equal to 0 seconds,
* then the following headers will be set:
* <ul>
* <li><code>Cache-Control: no-cache,no-store,must-revalidate</code></li>
* <li><code>Expires: [expiration date of 0]</code></li>
* <li><code>Pragma: no-cache</code></li>
* </ul>
*
* <h3>JSF development stage</h3>
* <p>To speed up development, caching by this filter is <strong>disabled</strong> when JSF project stage is set to
* <code>Development</code> as per {@link Servlets#isFacesDevelopment(javax.servlet.ServletContext)}.
*
* @author Bauke Scholtz
* @since 1.7
* @see HttpFilter
*/
public class CacheControlFilter extends HttpFilter {
// Constants ------------------------------------------------------------------------------------------------------
private static final String INIT_PARAM_EXPIRES = "expires";
private static final long DEFAULT_EXPIRES = 0;
private static final long DAYS_PER_WEEK = 7;
private static final String ERROR_EXPIRES = "The 'expires' init param must be a number between 0 and 999999999 with"
+ " optionally the 'w', 'd', 'h', 'm' or 's' suffix. For example: '6w' is 6 weeks. Default suffix is 's' for"
+ " seconds. For example: '86400' is 86400 seconds. Encountered an invalid value of '%s'.";
private enum Unit {
W(DAYS.toSeconds(DAYS_PER_WEEK)), D(DAYS.toSeconds(1)), H(HOURS.toSeconds(1)), M(MINUTES.toSeconds(1)), S(1);
private long seconds;
private Unit(long seconds) {
this.seconds = seconds;
}
public long toSeconds(long value) {
return value * seconds;
}
}
// Vars -----------------------------------------------------------------------------------------------------------
private long expires = DEFAULT_EXPIRES;
// Actions --------------------------------------------------------------------------------------------------------
/**
* Initialize the <code>expires</code> parameter.
*/
@Override
public void init() throws ServletException {
if (isFacesDevelopment(getServletContext())) {
return; // Don't cache during development.
}
String expiresParam = getInitParameter(INIT_PARAM_EXPIRES);
if (expiresParam != null) {
if (!expiresParam.matches("[0-9]{1,9}[wdhms]?")) {
throw new ServletException(format(ERROR_EXPIRES, expiresParam));
}
String[] parts = expiresParam.split("(?=[wdhms])");
long number = Long.parseLong(parts[0]);
if (parts.length > 1) {
String unit = parts[1];
number = Unit.valueOf(unit.toUpperCase()).toSeconds(number);
}
expires = number;
}
}
/**
* Set the necessary response headers based on <code>expires</code> initialization parameter.
*/
@Override
public void doFilter
(HttpServletRequest request, HttpServletResponse response, HttpSession session, FilterChain chain)
throws ServletException, IOException
{
if (!isFacesResourceRequest(request)) {
setCacheHeaders(response, expires);
}
chain.doFilter(request, response);
}
}