/*
* Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb.client;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.BodyDataSink;
import org.xlightweb.BodyForwarder;
import org.xlightweb.HttpRequest;
import org.xlightweb.HttpResponse;
import org.xlightweb.HttpUtils;
import org.xlightweb.IHttpExchange;
import org.xlightweb.IHttpRequest;
import org.xlightweb.IHttpRequestHandler;
import org.xlightweb.IHttpRequestHeader;
import org.xlightweb.IHttpResponse;
import org.xlightweb.IHttpResponseHandler;
import org.xlightweb.IHttpResponseHeader;
import org.xlightweb.InvokeOn;
import org.xlightweb.NonBlockingBodyDataSource;
import org.xlightweb.client.HttpCache.CacheEntry;
import org.xlightweb.client.HttpCache.IValidationHandler;
import org.xsocket.DataConverter;
import org.xsocket.Execution;
import org.xsocket.ILifeCycle;
/**
* Cache handler
*
* @author grro@xlightweb.org
*/
final class CacheHandler implements IHttpRequestHandler, ILifeCycle {
/**
* CookieHandler is unsynchronized by config. See HttpUtils$RequestHandlerInfo
*/
private static final Logger LOG = Logger.getLogger(CacheHandler.class.getName());
static final String XHEADER_NAME = "X-Cache";
static final String SKIP_CACHE_HANDLING = "org.xlighhtweb.client.cachehandler.skipcachehandling";
private final IHttpCache cache;
// statistics
private int countCacheHit = 0;
private int countCacheMiss = 0;
private long countCacheableResponse = 0;
private long countNonCacheableResponse = 0;
public CacheHandler(HttpClient httpClient, int maxSizeByte) {
cache = new HttpCache(httpClient);
cache.setMaxSize(maxSizeByte);
}
public void onInit() {
}
public void onDestroy() throws IOException {
cache.close();
}
void setSharedCache(boolean isSharedCache) {
cache.setSharedCache(isSharedCache);
}
boolean isSharedCache() {
return cache.isSharedCache();
}
void setMaxCacheSizeBytes(int sizeBytes) {
cache.setMaxSize(sizeBytes);
}
int getMaxCacheSizeBytes() {
return cache.getMaxSize();
}
int getCurrentCacheSizeBytes() {
return cache.getCurrentSize();
}
int getCountCacheHit() {
return countCacheHit;
}
int getCountCacheMiss() {
return countCacheMiss;
}
long getCountCacheableResponse() {
return countCacheableResponse;
}
long getNonCountCacheableResponse() {
return countNonCacheableResponse;
}
List<String> getCacheInfo() {
List<String> result = new ArrayList<String>();
for (CacheEntry entry : cache.getEntries()) {
result.add(entry.getRequest().getMethod() + " " + entry.getRequest().getRequestUrl().toString() + " (size " + DataConverter.toFormatedBytesSize(entry.getSize()) + ", age " + DataConverter.toFormatedDuration((System.currentTimeMillis() - entry.getCacheDate().getTime())) + ")");
}
return result;
}
/**
* {@inheritDoc}
*/
public void onRequest(final IHttpExchange exchange) throws IOException {
final IHttpRequest request = exchange.getRequest();
// skip cache handling?
if ((request.getAttribute(SKIP_CACHE_HANDLING) != null) && (request.getAttribute(SKIP_CACHE_HANDLING).equals("true"))) {
exchange.forward(request);
return;
}
// is request not cacheable?
if (!HttpCache.isCacheable(request)) {
exchange.forward(request);
return;
}
Date minFresh = new Date();
Date maxOld = null;
boolean isOnlyIfCached = false;
// handle requests cache control directive
String cacheControl = request.getHeader("Cache-Control");
if (cacheControl != null) {
for (String directive : cacheControl.split(",")) {
directive = directive.trim();
String directiveLower = directive.toLowerCase();
if (directive.equalsIgnoreCase("no-cache") || directive.equalsIgnoreCase("no-store")) {
exchange.forward(request);
return;
}
if (directiveLower.startsWith("min-fresh=")) {
String minRefresh = directive.substring("min-fresh=".length(), directive.length()).trim();
minFresh = new Date(System.currentTimeMillis() + HttpUtils.parseLong(minRefresh, 0));
}
if (directiveLower.startsWith("max-stale")) {
if (directive.length() > "max-stale=".length()) {
String maxStale = directive.substring("max-stale=".length(), directive.length()).trim();
minFresh = new Date(System.currentTimeMillis() - (1000L * (HttpUtils.parseLong(maxStale, 365 * 24 * 60 * 60))));
} else {
minFresh = new Date(System.currentTimeMillis() - (1000L * (365L * 24L * 60L * 60L)));
}
}
if (directiveLower.startsWith("max-age=")) {
String maxAge = directive.substring("max-age=".length(), directive.length()).trim();
maxOld = new Date(System.currentTimeMillis() - (1000L * HttpUtils.parseLong(maxAge, 0)));
}
if (directive.equalsIgnoreCase("only-if-cached")) {
isOnlyIfCached = true;
}
}
}
// handle caching
try {
CacheEntry ce = cache.get(request, minFresh);
// is cache entry valid?
if (ce != null) {
if ((maxOld != null) && (ce.isAfter(maxOld))) {
forwardForCache(exchange);
return;
}
// must revalidate?
if (ce.mustRevalidate(minFresh)) {
if (isOnlyIfCached) {
countCacheMiss++;
exchange.sendError(504);
} else {
IValidationHandler validationHdl = new IValidationHandler() {
public void onRevalidated(boolean isNotModified, CacheEntry ce) {
if (isNotModified) {
try {
countCacheHit++;
IHttpResponse resp = ce.newResponse();
resp.setHeader(XHEADER_NAME, "HIT - revalidated (xLightweb)");
exchange.send(resp);
} catch (IOException ioe) {
exchange.sendError(ioe);
}
} else {
try {
countCacheMiss++;
IHttpResponse resp = ce.newResponse();
exchange.send(resp);
} catch (IOException ioe) {
exchange.sendError(ioe);
}
}
}
public void onException(IOException ioe) {
exchange.sendError(ioe);
}
};
ce.revalidate(validationHdl);
}
// no, return cached response
} else {
countCacheHit++;
IHttpResponse resp = ce.newResponse();
resp.setHeader(XHEADER_NAME, "HIT (xLightweb)");
exchange.send(resp);
}
// no, forward request and intercept response
} else {
countCacheMiss++;
if (isOnlyIfCached) {
exchange.sendError(504);
} else {
forwardForCache(exchange);
}
}
} catch (IOException ioe) {
exchange.sendError(ioe);
}
}
private void forwardForCache(final IHttpExchange exchange) throws IOException {
IHttpRequest request = exchange.getRequest();
final IHttpRequestHeader headerCopy = request.getRequestHeader().copy();
final Interaction interaction = new Interaction();
if (request.hasBody()) {
final List<ByteBuffer> bodyCopy = new ArrayList<ByteBuffer>();
NonBlockingBodyDataSource dataSource = request.getNonBlockingBody();
ForwarderResponseHandler forwardResponseHandler = new ForwarderResponseHandler(interaction, exchange);
BodyDataSink dataSink = exchange.forward(request.getRequestHeader(), forwardResponseHandler);
BodyForwarder bodyForwarder = new BodyForwarder(dataSource, dataSink) {
int currentSize = 0;
@Override
public void onData(NonBlockingBodyDataSource bodyDataSource, BodyDataSink bodyDataSink) throws BufferUnderflowException, IOException {
ByteBuffer[] data = bodyDataSource.readByteBufferByLength(bodyDataSource.available());
if (currentSize < cache.getMaxSizeCacheEntry()) {
for (ByteBuffer buf : data) {
ByteBuffer bufCopy = buf.duplicate();
currentSize += bufCopy.remaining();
bodyCopy.add(bufCopy);
}
}
bodyDataSink.write(data);
}
@Override
public void onComplete() {
if (currentSize < cache.getMaxSizeCacheEntry()) {
try {
interaction.setRequest(new HttpRequest(headerCopy, bodyCopy));
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by creating/registering cachedResponse " + ioe.toString());
}
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("do not cache interaction (to large or request not complete)");
}
}
}
};
dataSource.setDataHandler(bodyForwarder);
} else {
interaction.setRequest(new HttpRequest(headerCopy));
ForwarderResponseHandler forwardResponseHandler = new ForwarderResponseHandler(interaction, exchange);
exchange.forward(exchange.getRequest(), forwardResponseHandler);
}
}
@Execution(Execution.NONTHREADED)
private final class ForwarderResponseHandler implements IHttpResponseHandler {
private final Interaction interaction;
private final IHttpExchange exchange;
public ForwarderResponseHandler(Interaction interaction, IHttpExchange exchange) {
this.interaction = interaction;
this.exchange = exchange;
}
@InvokeOn(InvokeOn.HEADER_RECEIVED)
public void onResponse(IHttpResponse response) throws IOException {
// is response cacheable?
if (HttpCache.isCacheable(response, isSharedCache())) {
final IHttpResponseHeader responseHeader = response.getResponseHeader();
if (response.hasBody()) {
final NonBlockingBodyDataSource dataSource = response.getNonBlockingBody();
BodyDataSink dataSink = exchange.send(responseHeader);
BodyForwarder bodyForwarder = new BodyForwarder(dataSource, dataSink) {
private final List<ByteBuffer> responseBodyCopy = new ArrayList<ByteBuffer>();
@Override
public void onData(NonBlockingBodyDataSource bodyDataSource, BodyDataSink bodyDataSink) throws BufferUnderflowException, IOException {
ByteBuffer[] data = bodyDataSource.readByteBufferByLength(bodyDataSource.available());
for (ByteBuffer buf : data) {
responseBodyCopy.add(buf.duplicate());
}
bodyDataSink.write(data);
}
@Override
public void onComplete() {
try {
IHttpResponse responseCopy = new HttpResponse(responseHeader.copy(), responseBodyCopy);
interaction.setResponse(responseCopy);
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by creating/registering cachedResponse " + ioe.toString());
}
}
}
};
dataSource.setDataHandler(bodyForwarder);
} else {
interaction.setResponse(HttpUtils.copy(response));
exchange.send(response);
}
// response is not cacheable
} else {
countNonCacheableResponse++;
exchange.send(response);
return;
}
}
public void onException(IOException ioe) throws IOException {
exchange.sendError(ioe);
}
}
private final class Interaction {
private IHttpRequest request = null;
private IHttpResponse response = null;
private final long startTime;
public Interaction() {
startTime = System.currentTimeMillis();
}
public synchronized void setRequest(IHttpRequest request) {
this.request = request;
if (response != null) {
addToCache();
}
}
public synchronized void setResponse(IHttpResponse response) {
this.response = response;
if (request != null) {
addToCache();
}
}
private void addToCache() {
try {
countCacheableResponse++;
cache.register(request, System.currentTimeMillis() - startTime, response);
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by adding interaction to cache " + ioe.toString());
}
}
request = null;
response = null;
}
}
}