com.twelvemonkeys.servlet.cache.HTTPCache Maven / Gradle / Ivy
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.servlet.cache;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.net.MIMEUtil;
import com.twelvemonkeys.net.NetUtil;
import com.twelvemonkeys.util.LRUHashMap;
import com.twelvemonkeys.util.LinkedMap;
import com.twelvemonkeys.util.NullMap;
import javax.servlet.ServletContext;
import java.io.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A "simple" HTTP cache.
*
*
* @author Harald Kuhr
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java#4 $
* @todo OMPTIMIZE: Cache parsed vary-info objects, not the properties-files
* @todo BUG: Better filename handling, as some filenames become too long..
* - Use a mix of parameters and hashcode + lenght with fixed (max) lenght?
* (Hashcodes of Strings are constant).
* - Store full filenames in .vary, instead of just extension, and use
* short filenames? (and only one .vary per dir).
*
*
* @todo TEST: Battle-testing using some URL-hammer tool and maybe a profiler
* @todo ETag/Conditional (If-None-Match) support!
* @todo Rewrite to use java.util.concurrent Locks (if possible) for performance
* Maybe use ConcurrentHashMap instead fo synchronized HashMap?
* @todo Rewrite to use NIO for performance
* @todo Allow no tempdir for in-memory only cache
* @todo Specify max size of disk-cache
*/
public class HTTPCache {
/**
* The HTTP header {@code "Cache-Control"}
*/
protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
/**
* The HTTP header {@code "Content-Type"}
*/
protected static final String HEADER_CONTENT_TYPE = "Content-Type";
/**
* The HTTP header {@code "Date"}
*/
protected static final String HEADER_DATE = "Date";
/**
* The HTTP header {@code "ETag"}
*/
protected static final String HEADER_ETAG = "ETag";
/**
* The HTTP header {@code "Expires"}
*/
protected static final String HEADER_EXPIRES = "Expires";
/**
* The HTTP header {@code "If-Modified-Since"}
*/
protected static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
/**
* The HTTP header {@code "If-None-Match"}
*/
protected static final String HEADER_IF_NONE_MATCH = "If-None-Match";
/**
* The HTTP header {@code "Last-Modified"}
*/
protected static final String HEADER_LAST_MODIFIED = "Last-Modified";
/**
* The HTTP header {@code "Pragma"}
*/
protected static final String HEADER_PRAGMA = "Pragma";
/**
* The HTTP header {@code "Vary"}
*/
protected static final String HEADER_VARY = "Vary";
/**
* The HTTP header {@code "Warning"}
*/
protected static final String HEADER_WARNING = "Warning";
/**
* HTTP extension header {@code "X-Cached-At"}
*/
protected static final String HEADER_CACHED_TIME = "X-Cached-At";
/**
* The file extension for header files ({@code ".headers"})
*/
protected static final String FILE_EXT_HEADERS = ".headers";
/**
* The file extension for varation-info files ({@code ".vary"})
*/
protected static final String FILE_EXT_VARY = ".vary";
protected static final int STATUS_OK = 200;
/**
* The directory used for the disk-based cache
*/
private File mTempDir;
/**
* Indicates wether the disk-based cache should be deleted when the
* container shuts down/VM exits
*/
private boolean mDeleteCacheOnExit;
/**
* In-memory content cache
*/
private final Map mContentCache;
/**
* In-memory enity cache
*/
private final Map mEntityCache;
/**
* In-memory varyiation-info cache
*/
private final Map mVaryCache;
private long mDefaultExpiryTime = -1;
private final Logger mLogger;
// Internal constructor for sublcasses only
protected HTTPCache(
final File pTempFolder,
final long pDefaultCacheExpiryTime,
final int pMaxMemCacheSize,
final int pMaxCachedEntites,
final boolean pDeleteCacheOnExit,
final Logger pLogger
) {
if (pTempFolder == null) {
throw new IllegalArgumentException("temp folder == null");
}
if (!pTempFolder.exists() && !pTempFolder.mkdirs()) {
throw new IllegalArgumentException("Could not create required temp directory: " + mTempDir.getAbsolutePath());
}
if (!(pTempFolder.canRead() && pTempFolder.canWrite())) {
throw new IllegalArgumentException("Must have read/write access to temp folder: " + mTempDir.getAbsolutePath());
}
if (pDefaultCacheExpiryTime < 0) {
throw new IllegalArgumentException("Negative expiry time");
}
if (pMaxMemCacheSize < 0) {
throw new IllegalArgumentException("Negative maximum memory cache size");
}
if (pMaxCachedEntites < 0) {
throw new IllegalArgumentException("Negative maximum number of cached entries");
}
mDefaultExpiryTime = pDefaultCacheExpiryTime;
if (pMaxMemCacheSize > 0) {
// Map backing = new SizedLRUMap(pMaxMemCacheSize); // size in bytes
// mContentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime);
mContentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes
}
else {
mContentCache = new NullMap();
}
mEntityCache = new LRUHashMap(pMaxCachedEntites);
mVaryCache = new LRUHashMap(pMaxCachedEntites);
mDeleteCacheOnExit = pDeleteCacheOnExit;
mTempDir = pTempFolder;
mLogger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName());
}
/**
* Creates an {@code HTTPCache}.
*
* @param pTempFolder the temp folder for this cache.
* @param pDefaultCacheExpiryTime Default expiry time for cached entities,
* {@code >= 0}
* @param pMaxMemCacheSize Maximum size of in-memory cache for content
* in bytes, {@code >= 0} ({@code 0} means no
* in-memory cache)
* @param pMaxCachedEntites Maximum number of entities in cache
* @param pDeleteCacheOnExit specifies wether the file cache should be
* deleted when the application or VM shuts down
* @throws IllegalArgumentException if {@code pName} or {@code pContext} is
* {@code null} or if any of {@code pDefaultCacheExpiryTime},
* {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are
* negative,
* or if the directory as given in the context attribute
* {@code "javax.servlet.context.tempdir"} does not exist, and
* cannot be created.
*/
public HTTPCache(final File pTempFolder,
final long pDefaultCacheExpiryTime,
final int pMaxMemCacheSize, final int pMaxCachedEntites,
final boolean pDeleteCacheOnExit) {
this(pTempFolder, pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, null);
}
/**
* Creates an {@code HTTPCache}.
*
* @param pName Name of this cache (should be unique per application).
* Used for temp folder
* @param pContext Servlet context for the application.
* @param pDefaultCacheExpiryTime Default expiry time for cached entities,
* {@code >= 0}
* @param pMaxMemCacheSize Maximum size of in-memory cache for content
* in bytes, {@code >= 0} ({@code 0} means no
* in-memory cache)
* @param pMaxCachedEntites Maximum number of entities in cache
* @param pDeleteCacheOnExit specifies wether the file cache should be
* deleted when the application or VM shuts down
* @throws IllegalArgumentException if {@code pName} or {@code pContext} is
* {@code null} or if any of {@code pDefaultCacheExpiryTime},
* {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are
* negative,
* or if the directory as given in the context attribute
* {@code "javax.servlet.context.tempdir"} does not exist, and
* cannot be created.
* @deprecated Use {@link #HTTPCache(File, long, int, int, boolean)} instead.
*/
public HTTPCache(final String pName, final ServletContext pContext,
final int pDefaultCacheExpiryTime, final int pMaxMemCacheSize,
final int pMaxCachedEntites, final boolean pDeleteCacheOnExit) {
this(
getTempFolder(pName, pContext),
pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit,
new CacheFilter.ServletContextLoggerAdapter(pName, pContext)
);
}
private static File getTempFolder(String pName, ServletContext pContext) {
if (pName == null) {
throw new IllegalArgumentException("name == null");
}
if (pName.trim().length() == 0) {
throw new IllegalArgumentException("Empty name");
}
if (pContext == null) {
throw new IllegalArgumentException("servlet context == null");
}
File tempRoot = (File) pContext.getAttribute("javax.servlet.context.tempdir");
if (tempRoot == null) {
throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\"");
}
return new File(tempRoot, pName);
}
public String toString() {
StringBuilder buf = new StringBuilder(getClass().getSimpleName());
buf.append("[");
buf.append("Temp dir: ");
buf.append(mTempDir.getAbsolutePath());
if (mDeleteCacheOnExit) {
buf.append(" (non-persistent)");
}
else {
buf.append(" (persistent)");
}
buf.append(", EntityCache: {");
buf.append(mEntityCache.size());
buf.append(" entries in a ");
buf.append(mEntityCache.getClass().getName());
buf.append("}, VaryCache: {");
buf.append(mVaryCache.size());
buf.append(" entries in a ");
buf.append(mVaryCache.getClass().getName());
buf.append("}, ContentCache: {");
buf.append(mContentCache.size());
buf.append(" entries in a ");
buf.append(mContentCache.getClass().getName());
buf.append("}]");
return buf.toString();
}
void log(final String pMessage) {
mLogger.log(Level.INFO, pMessage);
}
void log(final String pMessage, Throwable pException) {
mLogger.log(Level.WARNING, pMessage, pException);
}
/**
* Looks up the {@code CachedEntity} for the given request.
*
* @param pRequest the request
* @param pResponse the response
* @param pResolver the resolver
* @throws java.io.IOException if an I/O error occurs
* @throws CacheException if the cached entity can't be resolved for some reason
*/
public void doCached(final CacheRequest pRequest, final CacheResponse pResponse, final ResponseResolver pResolver) throws IOException, CacheException {
// TODO: Expire cached items on PUT/POST/DELETE/PURGE
// If not cachable request, resolve directly
if (!isCacheable(pRequest)) {
pResolver.resolve(pRequest, pResponse);
}
else {
// Generate cacheURI
String cacheURI = generateCacheURI(pRequest);
// System.out.println(" ## HTTPCache ## Request Id (cacheURI): " + cacheURI);
// Get/create cached entity
CachedEntity cached;
synchronized (mEntityCache) {
cached = mEntityCache.get(cacheURI);
if (cached == null) {
cached = new CachedEntityImpl(cacheURI, this);
mEntityCache.put(cacheURI, cached);
}
}
// else if (not cached ||?stale), resolve through wrapped (caching) response
// else render to response
// TODO: This is a bottleneck for uncachable resources. Should not
// synchronize, if we know (HOW?) the resource is not cachable.
synchronized (cached) {
if (cached.isStale(pRequest) /* TODO: NOT CACHED?! */) {
// Go fetch...
WritableCachedResponse cachedResponse = cached.createCachedResponse();
pResolver.resolve(pRequest, cachedResponse);
if (isCachable(cachedResponse)) {
// System.out.println("Registering content: " + cachedResponse.getCachedResponse());
registerContent(cacheURI, pRequest, cachedResponse.getCachedResponse());
}
else {
// TODO: What about non-cachable responses? We need to either remove them from cache, or mark them as stale...
// Best is probably to mark as non-cacheable for later, and NOT store content (performance)
// System.out.println("Non-cacheable response: " + cachedResponse);
// TODO: Write, but should really do this unbuffered.... And some resolver might be able to do just that?
// Might need a resolver.isWriteThroughForUncachableResources() method...
pResponse.setStatus(cachedResponse.getStatus());
cachedResponse.writeHeadersTo(pResponse);
cachedResponse.writeContentsTo(pResponse.getOutputStream());
return;
}
}
}
cached.render(pRequest, pResponse);
}
}
protected void invalidate(CacheRequest pRequest) {
// Generate cacheURI
String cacheURI = generateCacheURI(pRequest);
// Get/create cached entity
CachedEntity cached;
synchronized (mEntityCache) {
cached = mEntityCache.get(cacheURI);
if (cached != null) {
// TODO; Remove all variants
mEntityCache.remove(cacheURI);
}
}
}
private boolean isCacheable(final CacheRequest pRequest) {
// TODO: Support public/private cache (a cache probably have to be one of the two, when created)
// TODO: Only private caches should cache requests with Authorization
// TODO: OptimizeMe!
// It's probably best to cache the "cacheableness" of a request and a resource separately
List cacheControlValues = pRequest.getHeaders().get(HEADER_CACHE_CONTROL);
if (cacheControlValues != null) {
Map cacheControl = new HashMap();
for (String cc : cacheControlValues) {
List directives = Arrays.asList(cc.split(","));
for (String directive : directives) {
directive = directive.trim();
if (directive.length() > 0) {
String[] directiveParts = directive.split("=", 2);
cacheControl.put(directiveParts[0], directiveParts.length > 1 ? directiveParts[1] : null);
}
}
}
if (cacheControl.containsKey("no-cache") || cacheControl.containsKey("no-store")) {
return false;
}
/*
"no-cache" ; Section 14.9.1
| "no-store" ; Section 14.9.2
| "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4
| "max-stale" [ "=" delta-seconds ] ; Section 14.9.3
| "min-fresh" "=" delta-seconds ; Section 14.9.3
| "no-transform" ; Section 14.9.5
| "only-if-cached"
*/
}
return true;
}
private boolean isCachable(final CacheResponse pResponse) {
if (pResponse.getStatus() != STATUS_OK) {
return false;
}
// Vary: *
List values = pResponse.getHeaders().get(HTTPCache.HEADER_VARY);
if (values != null) {
for (String value : values) {
if ("*".equals(value)) {
return false;
}
}
}
// Cache-Control: no-cache, no-store, must-revalidate
values = pResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")
|| StringUtil.contains(value, "no-store")
|| StringUtil.contains(value, "must-revalidate")) {
return false;
}
}
}
// Pragma: no-cache
values = pResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA);
if (values != null) {
for (String value : values) {
if (StringUtil.contains(value, "no-cache")) {
return false;
}
}
}
return true;
}
/**
* Allows a server-side cache mechanism to peek at the real file.
* Default implementation return {@code null}.
*
* @param pRequest the request
* @return {@code null}, always
*/
protected File getRealFile(final CacheRequest pRequest) {
// TODO: Create callback for this? Only possible for server-side cache... Maybe we can get away without this?
// For now: Default implementation that returns null
return null;
/*
String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest);
// System.out.println(" ## HTTPCache ## Context relative URI: " + contextRelativeURI);
String path = mContext.getRealPath(contextRelativeURI);
// System.out.println(" ## HTTPCache ## Real path: " + path);
if (path != null) {
return new File(path);
}
return null;
*/
}
private File getCachedFile(final String pCacheURI, final CacheRequest pRequest) {
File file = null;
// Get base dir
File base = new File(mTempDir, "./" + pCacheURI);
final String basePath = base.getAbsolutePath();
File directory = base.getParentFile();
// Get list of files that are candidates
File[] candidates = directory.listFiles(new FileFilter() {
public boolean accept(File pFile) {
return pFile.getAbsolutePath().startsWith(basePath)
&& !pFile.getName().endsWith(FILE_EXT_HEADERS)
&& !pFile.getName().endsWith(FILE_EXT_VARY);
}
});
// Negotiation
if (candidates != null) {
String extension = getVaryExtension(pCacheURI, pRequest);
//System.out.println("-- Vary ext: " + extension);
if (extension != null) {
for (File candidate : candidates) {
//System.out.println("-- Candidate: " + candidates[i]);
if (extension.equals("ANY") || extension.equals(FileUtil.getExtension(candidate))) {
//System.out.println("-- Candidate selected");
file = candidate;
break;
}
}
}
}
else if (base.exists()) {
//System.out.println("-- File not a directory: " + directory);
log("File not a directory: " + directory);
}
return file;
}
private String getVaryExtension(final String pCacheURI, final CacheRequest pRequest) {
Properties variations = getVaryProperties(pCacheURI);
String[] varyHeaders = StringUtil.toStringArray(variations.getProperty(HEADER_VARY, ""));
// System.out.println("-- Vary: \"" + variations.getProperty(HEADER_VARY) + "\"");
String varyKey = createVaryKey(varyHeaders, pRequest);
// System.out.println("-- Vary key: \"" + varyKey + "\"");
// If no vary, just go with any version...
return StringUtil.isEmpty(varyKey) ? "ANY" : variations.getProperty(varyKey, null);
}
private String createVaryKey(final String[] pVaryHeaders, final CacheRequest pRequest) {
if (pVaryHeaders == null) {
return null;
}
StringBuilder headerValues = new StringBuilder();
for (String varyHeader : pVaryHeaders) {
List varies = pRequest.getHeaders().get(varyHeader);
String headerValue = varies != null && varies.size() > 0 ? varies.get(0) : null;
headerValues.append(varyHeader);
headerValues.append("__V_");
headerValues.append(createSafeHeader(headerValue));
}
return headerValues.toString();
}
private void storeVaryProperties(final String pCacheURI, final Properties pVariations) {
synchronized (pVariations) {
try {
File file = getVaryPropertiesFile(pCacheURI);
if (!file.exists() && mDeleteCacheOnExit) {
file.deleteOnExit();
}
FileOutputStream out = new FileOutputStream(file);
try {
pVariations.store(out, pCacheURI + " Vary info");
}
finally {
out.close();
}
}
catch (IOException ioe) {
log("Error: Could not store Vary info: " + ioe);
}
}
}
private Properties getVaryProperties(final String pCacheURI) {
Properties variations;
synchronized (mVaryCache) {
variations = mVaryCache.get(pCacheURI);
if (variations == null) {
variations = loadVaryProperties(pCacheURI);
mVaryCache.put(pCacheURI, variations);
}
}
return variations;
}
private Properties loadVaryProperties(final String pCacheURI) {
// Read Vary info, for content negotiation
Properties variations = new Properties();
File vary = getVaryPropertiesFile(pCacheURI);
if (vary.exists()) {
try {
FileInputStream in = new FileInputStream(vary);
try {
variations.load(in);
}
finally {
in.close();
}
}
catch (IOException ioe) {
log("Error: Could not load Vary info: " + ioe);
}
}
return variations;
}
private File getVaryPropertiesFile(final String pCacheURI) {
return new File(mTempDir, "./" + pCacheURI + FILE_EXT_VARY);
}
private static String generateCacheURI(final CacheRequest pRequest) {
StringBuilder buffer = new StringBuilder();
// Note: As the '/'s are not replaced, the directory structure will be recreated
// TODO: Old mehtod relied on context relativization, that must now be handled byt the ServletCacheRequest
// String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest);
String contextRelativeURI = pRequest.getRequestURI().getPath();
buffer.append(contextRelativeURI);
// Create directory for all resources
if (contextRelativeURI.charAt(contextRelativeURI.length() - 1) != '/') {
buffer.append('/');
}
// Get parameters from request, and recreate query to avoid unneccessary
// regeneration/caching when parameters are out of order
// Also makes caching work for POST
appendSortedRequestParams(pRequest, buffer);
return buffer.toString();
}
private static void appendSortedRequestParams(final CacheRequest pRequest, final StringBuilder pBuffer) {
Set names = pRequest.getParameters().keySet();
if (names.isEmpty()) {
pBuffer.append("defaultVersion");
return;
}
// We now have parameters
pBuffer.append('_'); // append '_' for '?', to avoid clash with default
// Create a sorted map
SortedMap> sortedQueryMap = new TreeMap>();
for (String name : names) {
List values = pRequest.getParameters().get(name);
sortedQueryMap.put(name, values);
}
// Iterate over sorted map, and append to stringbuffer
for (Iterator>> iterator = sortedQueryMap.entrySet().iterator(); iterator.hasNext();) {
Map.Entry> entry = iterator.next();
pBuffer.append(createSafe(entry.getKey()));
List values = entry.getValue();
if (values != null && values.size() > 0) {
pBuffer.append("_V"); // =
for (int i = 0; i < values.size(); i++) {
String value = values.get(i);
if (i != 0) {
pBuffer.append(',');
}
pBuffer.append(createSafe(value));
}
}
if (iterator.hasNext()) {
pBuffer.append("_P"); // &
}
}
}
private static String createSafe(final String pKey) {
return pKey.replace('/', '-')
.replace('&', '-') // In case they are encoded
.replace('#', '-')
.replace(';', '-');
}
private static String createSafeHeader(final String pHeaderValue) {
if (pHeaderValue == null) {
return "NULL";
}
return pHeaderValue.replace(' ', '_')
.replace(':', '_')
.replace('=', '_');
}
/**
* Registers content for the given URI in the cache.
*
* @param pCacheURI the cache URI
* @param pRequest the request
* @param pCachedResponse the cached response
* @throws IOException if the content could not be cached
*/
void registerContent(
final String pCacheURI,
final CacheRequest pRequest,
final CachedResponse pCachedResponse
) throws IOException {
// System.out.println(" ## HTTPCache ## Registering content for " + pCacheURI);
// pRequest.removeAttribute(ATTRIB_IS_STALE);
// pRequest.setAttribute(ATTRIB_CACHED_RESPONSE, pCachedResponse);
if ("HEAD".equals(pRequest.getMethod())) {
// System.out.println(" ## HTTPCache ## Was HEAD request, will NOT store content.");
return;
}
// TODO: Several resources may have same extension...
String extension = MIMEUtil.getExtension(pCachedResponse.getHeaderValue(HEADER_CONTENT_TYPE));
if (extension == null) {
extension = "[NULL]";
}
synchronized (mContentCache) {
mContentCache.put(pCacheURI + '.' + extension, pCachedResponse);
// This will be the default version
if (!mContentCache.containsKey(pCacheURI)) {
mContentCache.put(pCacheURI, pCachedResponse);
}
}
// Write the cached content to disk
File content = new File(mTempDir, "./" + pCacheURI + '.' + extension);
if (mDeleteCacheOnExit && !content.exists()) {
content.deleteOnExit();
}
File parent = content.getParentFile();
if (!(parent.exists() || parent.mkdirs())) {
log("Could not create directory " + parent.getAbsolutePath());
// TODO: Make sure vary-info is still created in memory
return;
}
OutputStream mContentStream = new BufferedOutputStream(new FileOutputStream(content));
try {
pCachedResponse.writeContentsTo(mContentStream);
}
finally {
try {
mContentStream.close();
}
catch (IOException e) {
log("Error closing content stream: " + e.getMessage(), e);
}
}
// Write the cached headers to disk (in pseudo-properties-format)
File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS);
if (mDeleteCacheOnExit && !headers.exists()) {
headers.deleteOnExit();
}
FileWriter writer = new FileWriter(headers);
PrintWriter headerWriter = new PrintWriter(writer);
try {
String[] names = pCachedResponse.getHeaderNames();
for (String name : names) {
String[] values = pCachedResponse.getHeaderValues(name);
headerWriter.print(name);
headerWriter.print(": ");
headerWriter.println(StringUtil.toCSVString(values, "\\"));
}
}
finally {
headerWriter.flush();
try {
writer.close();
}
catch (IOException e) {
log("Error closing header stream: " + e.getMessage(), e);
}
}
// TODO: Make this more robust, if some weird entity is not
// consistent in it's vary-headers..
// (sometimes Vary, sometimes not, or somtimes different Vary headers).
// Write extra Vary info to disk
String[] varyHeaders = pCachedResponse.getHeaderValues(HEADER_VARY);
// If no variations, then don't store vary info
if (varyHeaders != null && varyHeaders.length > 0) {
Properties variations = getVaryProperties(pCacheURI);
String vary = StringUtil.toCSVString(varyHeaders);
variations.setProperty(HEADER_VARY, vary);
// Create Vary-key and map to file extension...
String varyKey = createVaryKey(varyHeaders, pRequest);
// System.out.println("varyKey: " + varyKey);
// System.out.println("extension: " + extension);
variations.setProperty(varyKey, extension);
storeVaryProperties(pCacheURI, variations);
}
}
/**
* @param pCacheURI the cache URI
* @param pRequest the request
* @return a {@code CachedResponse} object
*/
CachedResponse getContent(final String pCacheURI, final CacheRequest pRequest) {
// System.err.println(" ## HTTPCache ## Looking up content for " + pCacheURI);
// Thread.dumpStack();
String extension = getVaryExtension(pCacheURI, pRequest);
CachedResponse response;
synchronized (mContentCache) {
// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + mContentCache /*.size()*/ + " entries)...");
if ("ANY".equals(extension)) {
response = mContentCache.get(pCacheURI);
}
else {
response = mContentCache.get(pCacheURI + '.' + extension);
}
if (response == null) {
// System.out.println(" ## HTTPCache ## Content not found in memory cache.");
//
// System.out.println(" ## HTTPCache ## Looking up content from disk cache...");
// Read from disk-cache
response = readFromDiskCache(pCacheURI, pRequest);
}
// if (response == null) {
// System.out.println(" ## HTTPCache ## Content not found in disk cache.");
// }
// else {
// System.out.println(" ## HTTPCache ## Content for " + pCacheURI + " found: " + response);
// }
}
return response;
}
private CachedResponse readFromDiskCache(String pCacheURI, CacheRequest pRequest) {
CachedResponse response = null;
try {
File content = getCachedFile(pCacheURI, pRequest);
if (content != null && content.exists()) {
// Read contents
byte[] contents = FileUtil.read(content);
// Read headers
File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS);
int headerSize = (int) headers.length();
BufferedReader reader = new BufferedReader(new FileReader(headers));
LinkedMap> headerMap = new LinkedMap>();
String line;
while ((line = reader.readLine()) != null) {
int colIdx = line.indexOf(':');
String name;
String value;
if (colIdx >= 0) {
name = line.substring(0, colIdx);
value = line.substring(colIdx + 2); // ": "
}
else {
name = line;
value = "";
}
headerMap.put(name, Arrays.asList(StringUtil.toStringArray(value, "\\")));
}
response = new CachedResponseImpl(STATUS_OK, headerMap, headerSize, contents);
mContentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response);
}
}
catch (IOException e) {
log("Error reading from cache: " + e.getMessage(), e);
}
return response;
}
boolean isContentStale(final String pCacheURI, final CacheRequest pRequest) {
// NOTE: Content is either stale or not, for the duration of one request, unless re-fetched
// Means that we must retry after a registerContent(), if caching as request-attribute
Boolean stale;
// stale = (Boolean) pRequest.getAttribute(ATTRIB_IS_STALE);
// if (stale != null) {
// return stale;
// }
stale = isContentStaleImpl(pCacheURI, pRequest);
// pRequest.setAttribute(ATTRIB_IS_STALE, stale);
return stale;
}
private boolean isContentStaleImpl(final String pCacheURI, final CacheRequest pRequest) {
CachedResponse response = getContent(pCacheURI, pRequest);
if (response == null) {
// System.out.println(" ## HTTPCache ## Content is stale (no content).");
return true;
}
// TODO: Get max-age=... from REQUEST too!
// TODO: What about time skew? Now should be (roughly) same as:
// long now = pRequest.getDateHeader("Date");
// TODO: If the time differs (server "now" vs client "now"), should we
// take that into consideration when testing for stale content?
// Probably, yes.
// TODO: Define rules for how to handle time skews
// Set timestamp check
// NOTE: HTTP Dates are always in GMT time zone
long now = (System.currentTimeMillis() / 1000L) * 1000L;
long expires = getDateHeader(response.getHeaderValue(HEADER_EXPIRES));
//long lastModified = getDateHeader(response, HEADER_LAST_MODIFIED);
long lastModified = getDateHeader(response.getHeaderValue(HEADER_CACHED_TIME));
// If expires header is not set, compute it
if (expires == -1L) {
/*
// Note: Not all content has Last-Modified header. We should then
// use lastModified() of the cached file, to compute expires time.
if (lastModified == -1L) {
File cached = getCachedFile(pCacheURI, pRequest);
if (cached != null && cached.exists()) {
lastModified = cached.lastModified();
//// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()");
}
}
*/
// If Cache-Control: max-age is present, use it, otherwise default
int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age");
if (maxAge == -1) {
expires = lastModified + mDefaultExpiryTime;
//// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry");
}
else {
expires = lastModified + (maxAge * 1000L); // max-age is seconds
//// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + maxAge");
}
}
/*
else {
// System.out.println(" ## HTTPCache ## Expires header is " + response.getHeaderValue(HEADER_EXPIRES));
}
*/
// Expired?
if (expires < now) {
// System.out.println(" ## HTTPCache ## Content is stale (content expired: "
// + NetUtil.formatHTTPDate(expires) + " before " + NetUtil.formatHTTPDate(now) + ").");
return true;
}
/*
if (lastModified == -1L) {
// Note: Not all content has Last-Modified header. We should then
// use lastModified() of the cached file, to compute expires time.
File cached = getCachedFile(pCacheURI, pRequest);
if (cached != null && cached.exists()) {
lastModified = cached.lastModified();
//// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()");
}
}
*/
// Get the real file for this request, if any
File real = getRealFile(pRequest);
//noinspection RedundantIfStatement
if (real != null && real.exists() && real.lastModified() > lastModified) {
// System.out.println(" ## HTTPCache ## Content is stale (new content"
// + NetUtil.formatHTTPDate(lastModified) + " before " + NetUtil.formatHTTPDate(real.lastModified()) + ").");
return true;
}
return false;
}
/**
* Parses a cached header with directive to an int.
* E.g: Cache-Control: max-age=60, returns 60
*
* @param pCached the cached response
* @param pHeaderName the header name (e.g: {@code CacheControl})
* @param pDirective the directive (e.g: {@code max-age}
* @return the int value, or {@code -1} if not found
*/
private int getIntHeader(final CachedResponse pCached, final String pHeaderName, final String pDirective) {
String[] headerValues = pCached.getHeaderValues(pHeaderName);
int value = -1;
if (headerValues != null) {
for (String headerValue : headerValues) {
if (pDirective == null) {
if (!StringUtil.isEmpty(headerValue)) {
value = Integer.parseInt(headerValue);
}
break;
}
else {
int start = headerValue.indexOf(pDirective);
// Directive found
if (start >= 0) {
int end = headerValue.lastIndexOf(',');
if (end < start) {
end = headerValue.length();
}
headerValue = headerValue.substring(start, end);
if (!StringUtil.isEmpty(headerValue)) {
value = Integer.parseInt(headerValue);
}
break;
}
}
}
}
return value;
}
/**
* Utility to read a date header from a cached response.
*
* @param pHeaderValue the header value
* @return the parsed date as a long, or {@code -1L} if not found
* @see javax.servlet.http.HttpServletRequest#getDateHeader(String)
*/
static long getDateHeader(final String pHeaderValue) {
long date = -1L;
if (pHeaderValue != null) {
date = NetUtil.parseHTTPDate(pHeaderValue);
}
return date;
}
// TODO: Extract and make public?
final static class SizedLRUMap extends LRUHashMap {
int mSize;
int mMaxSize;
public SizedLRUMap(int pMaxSize) {
//super(true);
super(); // Note: super.mMaxSize doesn't count...
mMaxSize = pMaxSize;
}
// In super (LRUMap?) this could just return 1...
protected int sizeOf(Object pValue) {
// HACK: As this is used as a backing for a TimeoutMap, the values
// will themselves be Entries...
while (pValue instanceof Map.Entry) {
pValue = ((Map.Entry) pValue).getValue();
}
CachedResponse cached = (CachedResponse) pValue;
return (cached != null ? cached.size() : 0);
}
@Override
public V put(K pKey, V pValue) {
mSize += sizeOf(pValue);
V old = super.put(pKey, pValue);
if (old != null) {
mSize -= sizeOf(old);
}
return old;
}
@Override
public V remove(Object pKey) {
V old = super.remove(pKey);
if (old != null) {
mSize -= sizeOf(old);
}
return old;
}
@Override
protected boolean removeEldestEntry(Map.Entry pEldest) {
if (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size
removeLRU();
}
return false;
}
@Override
public void removeLRU() {
while (mMaxSize <= mSize) { // NOTE: mMaxSize here is mem size
super.removeLRU();
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy