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