CacheAdapter.java

/*
 * 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.esigate.cache;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.Properties;

import org.apache.http.HttpException;
import org.apache.http.HttpStatus;
import org.apache.http.client.cache.CacheResponseStatus;
import org.apache.http.client.cache.HttpCacheContext;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.execchain.ClientExecChain;
import org.esigate.ConfigurationException;
import org.esigate.Parameters;
import org.esigate.http.DateUtils;
import org.esigate.http.OutgoingRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is changes the behavior of the HttpCache by transforming the headers in the requests or response.
 * 
 * @author Francois-Xavier Bonnet
 * 
 */
public class CacheAdapter {
    private static final Logger LOG = LoggerFactory.getLogger(CacheAdapter.class);
    private int staleIfError;
    private int staleWhileRevalidate;
    private int ttl;
    private boolean xCacheHeader;
    private boolean viaHeader;

    /**
     * Inititalize the instance.
     * 
     * @param properties
     *            properties
     */
    public void init(Properties properties) {
        staleIfError = Parameters.STALE_IF_ERROR.getValue(properties);
        staleWhileRevalidate = Parameters.STALE_WHILE_REVALIDATE.getValue(properties);
        int maxAsynchronousWorkers = Parameters.MAX_ASYNCHRONOUS_WORKERS.getValue(properties);
        if (staleWhileRevalidate > 0 && maxAsynchronousWorkers == 0) {
            throw new ConfigurationException("You must set a positive value for maxAsynchronousWorkers "
                    + "in order to enable background revalidation (staleWhileRevalidate)");
        }
        ttl = Parameters.TTL.getValue(properties);
        xCacheHeader = Parameters.X_CACHE_HEADER.getValue(properties);
        viaHeader = Parameters.VIA_HEADER.getValue(properties);
        LOG.info("Initializing cache for provider " + Arrays.toString(Parameters.REMOTE_URL_BASE.getValue(properties))
                + " staleIfError=" + staleIfError + " staleWhileRevalidate=" + staleWhileRevalidate + " ttl=" + ttl
                + " xCacheHeader=" + xCacheHeader + " viaHeader=" + viaHeader);
    }

    public ClientExecChain wrapCachingHttpClient(final ClientExecChain wrapped) {
        return new ClientExecChain() {

            /**
             * Removes client http cache directives like "Cache-control" and "Pragma". Users must not be able to bypass
             * the cache just by making a refresh in the browser. Generates X-cache header.
             * 
             */
            @Override
            public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
                    HttpClientContext httpClientContext, HttpExecutionAware execAware) throws IOException,
                    HttpException {
                OutgoingRequestContext context = OutgoingRequestContext.adapt(httpClientContext);

                // Switch route for the cache to generate the right cache key
                CloseableHttpResponse response = wrapped.execute(route, request, context, execAware);

                // Remove previously added Cache-control header
                if (request.getRequestLine().getMethod().equalsIgnoreCase("GET")
                        && (staleWhileRevalidate > 0 || staleIfError > 0)) {
                    response.removeHeader(response.getLastHeader("Cache-control"));
                }
                // Add X-cache header
                if (xCacheHeader) {
                    if (context != null) {
                        CacheResponseStatus cacheResponseStatus =
                                (CacheResponseStatus) context.getAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS);
                        String xCacheString;
                        if (cacheResponseStatus.equals(CacheResponseStatus.CACHE_HIT)) {
                            xCacheString = "HIT";
                        } else if (cacheResponseStatus.equals(CacheResponseStatus.VALIDATED)) {
                            xCacheString = "VALIDATED";
                        } else {
                            xCacheString = "MISS";
                        }
                        xCacheString += " from " + route.getTargetHost().toHostString();
                        xCacheString +=
                                " (" + request.getRequestLine().getMethod() + " " + request.getRequestLine().getUri()
                                        + ")";
                        response.addHeader("X-Cache", xCacheString);
                    }
                }

                // Remove Via header
                if (!viaHeader && response.containsHeader("Via")) {
                    response.removeHeaders("Via");
                }
                return response;
            }
        };
    }

    public ClientExecChain wrapBackendHttpClient(final ClientExecChain wrapped) {
        return new ClientExecChain() {

            private boolean isCacheableStatus(int statusCode) {
                return (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MOVED_PERMANENTLY
                        || statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_NOT_FOUND
                        || statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
                        || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE || statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT);
            }

            /**
             * Fire pre-fetch and post-fetch events Enables cache for all GET requests if cache ttl was forced to a
             * certain duration in the configuration. This is done even for non 200 return codes! This is a very
             * aggressive but efficient caching policy. Adds "stale-while-revalidate" and "stale-if-error" cache-control
             * directives depending on the configuration.
             * 
             * @throws HttpException
             * @throws IOException
             */
            @Override
            public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
                    HttpClientContext httpClientContext, HttpExecutionAware execAware) throws IOException,
                    HttpException {
                OutgoingRequestContext context = OutgoingRequestContext.adapt(httpClientContext);

                CloseableHttpResponse response = wrapped.execute(route, request, context, execAware);

                String method = request.getRequestLine().getMethod();
                int statusCode = response.getStatusLine().getStatusCode();

                // If ttl is set, force caching even for error pages
                if (ttl > 0 && method.equalsIgnoreCase("GET") && isCacheableStatus(statusCode)) {
                    response.removeHeaders("Date");
                    response.removeHeaders("Cache-control");
                    response.removeHeaders("Expires");
                    response.setHeader("Date", DateUtils.formatDate(new Date(System.currentTimeMillis())));
                    response.setHeader("Cache-control", "public, max-age=" + ttl);
                    response.setHeader("Expires",
                            DateUtils.formatDate(new Date(System.currentTimeMillis() + ((long) ttl) * 1000)));
                }
                if (request.getRequestLine().getMethod().equalsIgnoreCase("GET")) {
                    String cacheControlHeader = "";
                    if (staleWhileRevalidate > 0) {
                        cacheControlHeader += "stale-while-revalidate=" + staleWhileRevalidate;
                    }
                    if (staleIfError > 0) {
                        if (cacheControlHeader.length() > 0) {
                            cacheControlHeader += ",";
                        }
                        cacheControlHeader += "stale-if-error=" + staleIfError;
                    }
                    if (cacheControlHeader.length() > 0) {
                        response.addHeader("Cache-control", cacheControlHeader);
                    }
                }

                return response;
            }

        };
    }
}