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.
    }

}
