package org.ovirt.engine.ui.frontend.server.gwt; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Pattern; 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.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; /** * Controls caching of GWT application resources via HTTP response headers. * <p> * This filter works with following types of resources: * <ul> * <li>resources intended to be cached forever on the client ({@code cache} init-param) * <li>resources which always need to be checked for changes ({@code no-cache} init-param) * <li>resources not intended to be cached on the client at all ({@code no-store} init-param) * </ul> * <p> * Following table outlines cache control headers used with supported init-params: * * <blockquote> * <table border="1" cellpadding="5" cellspacing="0"> * <thead> * <tr> * <th>Init-param</th> * <th>Cache control headers</th> * </tr> * </thead> * <tbody> * <tr> * <td>{@code cache}</td> * <td> * <p> * <ul> * <li>{@code Expires: <nowPlusOneYear>} * <li>{@code Cache-Control: max-age=<oneYear>, public} * <li>{@code Pragma: <emptyString>} * <li>allow setting conditional download headers: {@code Etag}, {@code Last-Modified} * </ul> * </td> * </tr> * <tr> * <td>{@code no-cache}</td> * <td> * <p> * <ul> * <li>{@code Expires: <nowMinusOneDay>} * <li>{@code Cache-Control: no-cache} * <li>{@code Pragma: no-cache} * <li>allow setting conditional download headers: {@code Etag}, {@code Last-Modified} * </ul> * </td> * </tr> * <tr> * <td>{@code no-store}</td> * <td> * <p> * <ul> * <li>{@code Expires: <nowMinusOneDay>} * <li>{@code Cache-Control: no-cache, no-store, must-revalidate} * <li>{@code Pragma: no-cache} * <li>prevent setting conditional download headers: {@code Etag}, {@code Last-Modified} * </ul> * </td> * </tr> * </tbody> * </table> * </blockquote> * * <p> * Aside from conditional download headers, this filter prevents further modification of {@code Expires}, * {@code Cache-Control} and {@code Pragma} headers via response wrapper. */ public class GwtCachingFilter implements Filter { protected static final String CACHE_INIT_PARAM = "cache"; //$NON-NLS-1$ protected static final String NO_CACHE_INIT_PARAM = "no-cache"; //$NON-NLS-1$ protected static final String NO_STORE_INIT_PARAM = "no-store"; //$NON-NLS-1$ protected static final String CACHE_CONTROL_HEADER = "Cache-Control"; //$NON-NLS-1$ protected static final String EXPIRES_HEADER = "Expires"; //$NON-NLS-1$ protected static final String PRAGMA_HEADER = "Pragma"; //$NON-NLS-1$ protected static final String ETAG_HEADER = "Etag"; //$NON-NLS-1$ protected static final String LAST_MODIFIED_HEADER = "Last-Modified"; //$NON-NLS-1$ protected static final String CACHE_YEAR = "max-age=31556926, public"; //$NON-NLS-1$ protected static final String NO_CACHE = "no-cache"; //$NON-NLS-1$ protected static final String NO_STORE = "no-cache, no-store, must-revalidate"; //$NON-NLS-1$ protected static final String EMPTY_STRING = ""; //$NON-NLS-1$ private static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z"; //$NON-NLS-1$ private static final String GMT = "GMT"; //$NON-NLS-1$ private static Pattern cachePattern; private static Pattern noCachePattern; private static Pattern noStorePattern; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; if (cacheFilterPatternMatches(httpRequest)) { httpResponse.setHeader(EXPIRES_HEADER, getNowPlusYearHttpDate(Calendar.getInstance())); httpResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_YEAR); httpResponse.setHeader(PRAGMA_HEADER, EMPTY_STRING); httpResponse = getCacheHeaderResponseWrapper(httpResponse, false); } else if (noCacheFilterPatternMatches(httpRequest)) { httpResponse.setHeader(EXPIRES_HEADER, getYesterdayHttpDate(Calendar.getInstance())); httpResponse.setHeader(CACHE_CONTROL_HEADER, NO_CACHE); httpResponse.setHeader(PRAGMA_HEADER, NO_CACHE); httpResponse = getCacheHeaderResponseWrapper(httpResponse, false); } else if (noStoreFilterPatternMatches(httpRequest)) { httpResponse.setHeader(EXPIRES_HEADER, getYesterdayHttpDate(Calendar.getInstance())); httpResponse.setHeader(CACHE_CONTROL_HEADER, NO_STORE); httpResponse.setHeader(PRAGMA_HEADER, NO_CACHE); httpResponse = getCacheHeaderResponseWrapper(httpResponse, true); } chain.doFilter(request, httpResponse); } protected String getNowPlusYearHttpDate(Calendar calendar) { // Add a year to now. calendar.add(Calendar.YEAR, 1); // Format in the correct format. return formatUsLocaleGMTZone(calendar); } protected String getYesterdayHttpDate(Calendar calendar) { // Subtract a day, so it is in the past. calendar.add(Calendar.DAY_OF_MONTH, -1); // Format in the correct format. return formatUsLocaleGMTZone(calendar); } /** * Format the US locale in the GMT timezone. * @param calendar The {@code Calender} object to format. * @return A {@code String} containing the formatted date. */ private String formatUsLocaleGMTZone(final Calendar calendar) { SimpleDateFormat dateFormat = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone(GMT)); return dateFormat.format(calendar.getTime()); } private HttpServletResponseWrapper getCacheHeaderResponseWrapper(final HttpServletResponse httpResponse, final boolean preventConditionalDownloadHeaders) { return new HttpServletResponseWrapper(httpResponse) { @Override public void setHeader(String name, String value) { // Prevent setting Expires, Cache-Control and Pragma headers. if (EXPIRES_HEADER.equalsIgnoreCase(name) || CACHE_CONTROL_HEADER.equalsIgnoreCase(name) || PRAGMA_HEADER.equalsIgnoreCase(name)) { return; } // Prevent setting conditional download headers, if necessary. if (preventConditionalDownloadHeaders && (ETAG_HEADER.equalsIgnoreCase(name) || LAST_MODIFIED_HEADER.equalsIgnoreCase(name))) { return; } httpResponse.setHeader(name, value); } }; } protected boolean cacheFilterPatternMatches(HttpServletRequest httpRequest) { return cachePattern != null ? cachePattern.matcher(httpRequest.getRequestURI()).matches() : false; } protected boolean noCacheFilterPatternMatches(HttpServletRequest httpRequest) { return noCachePattern != null ? noCachePattern.matcher(httpRequest.getRequestURI()).matches() : false; } protected boolean noStoreFilterPatternMatches(HttpServletRequest httpRequest) { return noStorePattern != null ? noStorePattern.matcher(httpRequest.getRequestURI()).matches() : false; } @Override public void init(FilterConfig filterConfig) throws ServletException { // No need to worry about concurrency, worst case scenario // the same pattern is calculated a couple of times. if (cachePattern == null) { cachePattern = compilePatternFromInitParam(filterConfig, CACHE_INIT_PARAM); } if (noCachePattern == null) { noCachePattern = compilePatternFromInitParam(filterConfig, NO_CACHE_INIT_PARAM); } if (noStorePattern == null) { noStorePattern = compilePatternFromInitParam(filterConfig, NO_STORE_INIT_PARAM); } } private Pattern compilePatternFromInitParam(FilterConfig filterConfig, String name) { String paramValue = filterConfig.getInitParameter(name); return paramValue != null ? Pattern.compile(paramValue) : null; } @Override public void destroy() { // Do nothing. } }