/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
*
* 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.onebusaway.presentation.services.cachecontrol;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateUtil;
import org.apache.struts2.ServletActionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class CacheControlInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = 1L;
private static Logger _log = LoggerFactory.getLogger(CacheControlInterceptor.class);
@Override
public String intercept(ActionInvocation invocation) throws Exception {
CacheControl cacheControl = getCacheControlAnnotation(invocation);
if (cacheControl == null)
return invocation.invoke();
HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
applyCacheControlHeader(invocation, cacheControl, request, response);
applyExpiresHeader(invocation, cacheControl, request, response);
applyETagHeader(invocation, cacheControl, request, response);
Date lastModifiedTime = applyLastModifiedHeader(invocation, cacheControl,
request, response);
Map<String, String> requestCacheControl = parseRequestCacheControl(request);
boolean bypassCache = bypassCache(requestCacheControl);
if (lastModifiedTime != null && !bypassCache) {
String modifiedSinceValue = request.getHeader("if-modified-since");
if (modifiedSinceValue != null) {
try {
Date modifiedSince = DateUtil.parseDate(modifiedSinceValue);
if (!lastModifiedTime.after(modifiedSince)) {
response.setStatus(304);
return null;
}
} catch (DateParseException ex) {
}
}
}
/**
* If we have a HEAD request and the action has specified a short-circuit
* response, skip the rest of the action chain
*/
String method = request.getMethod();
if (cacheControl.shortCircuit() && "HEAD".equals(method))
return null;
return invocation.invoke();
}
private boolean bypassCache(Map<String, String> requestCacheControl) {
return requestCacheControl.containsKey("no-cache");
}
private Map<String, String> parseRequestCacheControl(
HttpServletRequest request) {
String value = request.getHeader("cache-control");
if (value == null)
return Collections.emptyMap();
Map<String, String> m = new HashMap<String, String>();
for (String kvp : value.split(",")) {
kvp = kvp.trim();
if (kvp.length() == 0)
continue;
int index = kvp.indexOf('=');
if (index == -1)
m.put(kvp.toLowerCase(), null);
else {
String key = kvp.substring(0, index).toLowerCase();
m.put(key, kvp.substring(index + 1));
}
}
return m;
}
private void applyCacheControlHeader(ActionInvocation invocation,
CacheControl cacheControl, HttpServletRequest request,
HttpServletResponse response) {
StringBuilder b = new StringBuilder();
if (cacheControl.isPublic()) {
delimiter(b);
b.append("public");
}
if (cacheControl.isPrivate()) {
delimiter(b);
b.append("private");
}
if (cacheControl.noCache()) {
delimiter(b);
b.append("no-cache");
}
if (cacheControl.noStore()) {
delimiter(b);
b.append("no-store");
}
if (cacheControl.noTransform()) {
delimiter(b);
b.append("no-transform");
}
if (cacheControl.maxAge() != -1) {
delimiter(b);
b.append("max-age=").append(cacheControl.maxAge());
}
if (cacheControl.sharedMaxAge() != -1) {
delimiter(b);
b.append("s-maxage=").append(cacheControl.sharedMaxAge());
}
if (b.length() > 0) {
response.setHeader("Cache-Control", b.toString());
}
}
private void applyExpiresHeader(ActionInvocation invocation,
CacheControl cacheControl, HttpServletRequest request,
HttpServletResponse response) {
Date expiresTime = null;
if (cacheControl.expiresOffset() > 0) {
expiresTime = new Date(System.currentTimeMillis()
+ cacheControl.expiresOffset());
} else {
expiresTime = invokeDateMethod(invocation, cacheControl.expiresMethod());
}
if (expiresTime == null)
return;
String expiresTimeAsString = DateUtil.formatDate(expiresTime);
response.setHeader("Expires", expiresTimeAsString);
}
private Date applyLastModifiedHeader(ActionInvocation invocation,
CacheControl cacheControl, HttpServletRequest request,
HttpServletResponse response) {
Date lastModifiedTime = invokeDateMethod(invocation,
cacheControl.lastModifiedMethod());
if (lastModifiedTime == null)
return null;
String lastModifiedTimeAsString = DateUtil.formatDate(lastModifiedTime);
response.setHeader("Last-Modified", lastModifiedTimeAsString);
return lastModifiedTime;
}
private void applyETagHeader(ActionInvocation invocation,
CacheControl cacheControl, HttpServletRequest request,
HttpServletResponse response) {
String etagMethod = cacheControl.etagMethod();
if (etagMethod == null || "".equals(etagMethod))
return;
Object action = invocation.getAction();
Class<?> actionClass = action.getClass();
Method m = getMethod(actionClass, etagMethod);
if (m == null) {
_log.warn("etagMethod=\"" + etagMethod
+ "\" not found for actionClass=\"" + actionClass.getName() + "\"");
return;
}
Object result = invokeMethod(m, action);
if (result == null)
return;
response.setHeader("ETag", result.toString());
}
private CacheControl getCacheControlAnnotation(ActionInvocation invocation) {
Object action = invocation.getAction();
Class<? extends Object> actionClass = action.getClass();
ActionProxy proxy = invocation.getProxy();
String methodName = proxy.getMethod();
if (methodName != null) {
try {
Method m = actionClass.getMethod(methodName);
if (m != null) {
CacheControl methodCacheControl = m.getAnnotation(CacheControl.class);
if (methodCacheControl != null)
return methodCacheControl;
}
} catch (Exception ex) {
_log.warn("error searching for action method=\"" + methodName
+ "\" on action class=\"" + actionClass.getName() + "\"", ex);
}
}
return actionClass.getAnnotation(CacheControl.class);
}
private Date invokeDateMethod(ActionInvocation invocation, String methodName) {
if (methodName == null || "".equals(methodName))
return null;
Object action = invocation.getAction();
Class<?> actionClass = action.getClass();
Method m = getMethod(actionClass, methodName);
if (m == null) {
_log.warn("method=\"" + methodName + "\" not found for actionClass=\""
+ actionClass.getName() + "\"");
return null;
}
Object result = invokeMethod(m, action);
if (result == null)
return null;
if (result instanceof Date)
return (Date) result;
if (result instanceof Long)
return new Date(((Long) result).longValue());
_log.warn("unknown date value: " + result + " from method=\"" + methodName
+ "\" in actionClass=" + actionClass.getName());
return null;
}
private static final void delimiter(StringBuilder b) {
if (b.length() > 0)
b.append(", ");
}
private Method getMethod(Class<?> actionClass, String methodName,
Class<?>... parameterTypes) {
try {
return actionClass.getMethod(methodName, parameterTypes);
} catch (Exception ex) {
_log.warn("error searching for action method=\"" + methodName
+ "\" on action class=\"" + actionClass.getName() + "\"", ex);
return null;
}
}
private Object invokeMethod(Method method, Object target, Object... args) {
try {
return method.invoke(target, args);
} catch (Exception ex) {
_log.warn("error invoking method", ex);
return null;
}
}
}