/*
* Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.grizzly.http.server.filecache;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.http.CompressionConfig;
import org.glassfish.grizzly.http.HttpRequestPacket;
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.server.util.SimpleDateFormats;
import org.glassfish.grizzly.http.util.ContentType;
import org.glassfish.grizzly.http.util.FastHttpDateFormat;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.grizzly.http.util.MimeHeaders;
import org.glassfish.grizzly.localization.LogMessages;
import org.glassfish.grizzly.monitoring.DefaultMonitoringConfig;
import org.glassfish.grizzly.monitoring.MonitoringAware;
import org.glassfish.grizzly.monitoring.MonitoringConfig;
import org.glassfish.grizzly.monitoring.MonitoringUtils;
import org.glassfish.grizzly.utils.DelayedExecutor;
This class implements a file caching mechanism used to cache static resources.
Author: Jeanfrancois Arcand, Scott Oaks
/**
* This class implements a file caching mechanism used to cache static resources.
*
* @author Jeanfrancois Arcand
* @author Scott Oaks
*/
public class FileCache implements MonitoringAware<FileCacheProbe> {
private static final File TMP_DIR = new File(System.getProperty("java.io.tmpdir"));
final static String[] COMPRESSION_ALIASES = { "gzip" };
public enum CacheType {
HEAP, MAPPED, FILE, TIMESTAMP
}
public enum CacheResult {
OK_CACHED, OK_CACHED_TIMESTAMP, FAILED_CACHE_FULL, FAILED_ENTRY_EXISTS, FAILED
}
private static final Logger LOGGER = Grizzly.logger(FileCache.class);
Cache size.
/**
* Cache size.
*/
private final AtomicInteger cacheSize = new AtomicInteger();
A ByteBuffer
cache of static pages. /**
* A {@link ByteBuffer} cache of static pages.
*/
private final ConcurrentMap<FileCacheKey, FileCacheEntry> fileCacheMap = new ConcurrentHashMap<>();
private final FileCacheEntry NULL_CACHE_ENTRY = new FileCacheEntry(this);
Specifies the maximum time in seconds a resource may be cached.
/**
* Specifies the maximum time in seconds a resource may be cached.
*/
private int secondsMaxAge = -1;
The maximum entries in the FileCache
/**
* The maximum entries in the {@link FileCache}
*/
private volatile int maxCacheEntries = 1024;
The maximum size of a cached resource.
/**
* The maximum size of a cached resource.
*/
private long minEntrySize = Long.MIN_VALUE;
The maximum size of a cached resource.
/**
* The maximum size of a cached resource.
*/
private long maxEntrySize = Long.MAX_VALUE;
The maximum memory mapped bytes.
/**
* The maximum memory mapped bytes.
*/
private volatile long maxLargeFileCacheSize = Long.MAX_VALUE;
The maximum cached bytes
/**
* The maximum cached bytes
*/
private volatile long maxSmallFileCacheSize = 1048576;
The current cache size in bytes
/**
* The current cache size in bytes
*/
private final AtomicLong mappedMemorySize = new AtomicLong();
The current cache size in bytes
/**
* The current cache size in bytes
*/
private final AtomicLong heapSize = new AtomicLong();
Is the file cache enabled.
/**
* Is the file cache enabled.
*/
private boolean enabled = true;
private DelayedExecutor.DelayQueue<FileCacheEntry> delayQueue;
Folder to store compressed cached files
/**
* Folder to store compressed cached files
*/
private volatile File compressedFilesFolder = TMP_DIR;
Compression configuration, used to decide if cached resource has to be compressed or not
/**
* Compression configuration, used to decide if cached resource has to be compressed or not
*/
private final CompressionConfig compressionConfig = new CompressionConfig();
true, if zero-copy file-send feature could be used, or false otherwise.
/**
* <tt>true</tt>, if zero-copy file-send feature could be used, or <tt>false</tt> otherwise.
*/
private boolean fileSendEnabled;
File cache probes
/**
* File cache probes
*/
protected final DefaultMonitoringConfig<FileCacheProbe> monitoringConfig = new DefaultMonitoringConfig<FileCacheProbe>(FileCacheProbe.class) {
@Override
public Object createManagementObject() {
return createJmxManagementObject();
}
};
// ---------------------------------------------------- Methods ----------//
public void initialize(final DelayedExecutor delayedExecutor) {
delayQueue = delayedExecutor.createDelayQueue(new EntryWorker(), new EntryResolver());
}
Add a resource to the cache. Unlike the add(HttpRequestPacket, File)
this method adds a resource to a cache but is not able to send the resource content to a client if client doesn't have the latest version of this resource. /**
* Add a resource to the cache. Unlike the {@link #add(org.glassfish.grizzly.http.HttpRequestPacket, java.io.File)} this
* method adds a resource to a cache but is not able to send the resource content to a client if client doesn't have the
* latest version of this resource.
*/
public CacheResult add(final HttpRequestPacket request, final long lastModified) {
return add(request, null, lastModified);
}
/**
* Add a {@link File} resource to the cache. If a client comes with not the latest version of this resource - the
* {@link FileCache} will return it the latest resource version.
*/
public CacheResult add(final HttpRequestPacket request, final File cacheFile) {
return add(request, cacheFile, cacheFile.lastModified());
}
Add a resource to the cache.
/**
* Add a resource to the cache.
*/
protected CacheResult add(final HttpRequestPacket request, final File cacheFile, final long lastModified) {
final String requestURI = request.getRequestURI();
if (requestURI == null) {
return CacheResult.FAILED;
}
final String host = request.getHeader(Header.Host);
final FileCacheKey key = new FileCacheKey(host, requestURI);
if (fileCacheMap.putIfAbsent(key, NULL_CACHE_ENTRY) != null) {
key.recycle();
return CacheResult.FAILED_ENTRY_EXISTS;
}
final int size = cacheSize.incrementAndGet();
// cache is full.
if (size > getMaxCacheEntries()) {
cacheSize.decrementAndGet();
fileCacheMap.remove(key);
key.recycle();
return CacheResult.FAILED_CACHE_FULL;
}
final HttpResponsePacket response = request.getResponse();
final MimeHeaders headers = response.getHeaders();
final String contentType = response.getContentType();
final FileCacheEntry entry;
if (cacheFile != null) { // If we have a file - try to create File-aware cache resource
entry = createEntry(cacheFile);
entry.setCanBeCompressed(canBeCompressed(cacheFile, contentType));
} else {
entry = new FileCacheEntry(this);
entry.type = CacheType.TIMESTAMP;
}
entry.key = key;
entry.requestURI = requestURI;
entry.lastModified = lastModified;
entry.contentType = ContentType.newContentType(contentType);
entry.xPoweredBy = headers.getHeader(Header.XPoweredBy);
entry.date = headers.getHeader(Header.Date);
entry.lastModifiedHeader = headers.getHeader(Header.LastModified);
entry.host = host;
entry.Etag = headers.getHeader(Header.ETag);
entry.server = headers.getHeader(Header.Server);
fileCacheMap.put(key, entry);
notifyProbesEntryAdded(this, entry);
final int secondsMaxAgeLocal = getSecondsMaxAge();
if (secondsMaxAgeLocal > 0) {
delayQueue.add(entry, secondsMaxAgeLocal, TimeUnit.SECONDS);
}
return entry.type == CacheType.TIMESTAMP ? CacheResult.OK_CACHED_TIMESTAMP : CacheResult.OK_CACHED;
}
Returns FileCacheEntry
. If FileCacheEntry
has been found - this method also sets correspondent HttpResponsePacket
status code and reason phrase. /**
* Returns {@link FileCacheEntry}. If {@link FileCacheEntry} has been found - this method also sets correspondent
* {@link HttpResponsePacket} status code and reason phrase.
*/
public FileCacheEntry get(final HttpRequestPacket request) {
// It should be faster than calculating the key hash code
if (cacheSize.get() == 0) {
return null;
}
final LazyFileCacheKey key = LazyFileCacheKey.create(request);
final FileCacheEntry entry = fileCacheMap.get(key);
key.recycle();
try {
if (entry != null && entry != NULL_CACHE_ENTRY) {
// determine if we need to send the cache entry bytes
// to the user-agent
final HttpStatus httpStatus = checkIfHeaders(entry, request);
final boolean flushBody = httpStatus == null;
if (flushBody && entry.type == CacheType.TIMESTAMP) {
return null; // this will cause control to be passed to the static handler
}
request.getResponse().setStatus(httpStatus != null ? httpStatus : HttpStatus.OK_200);
notifyProbesEntryHit(this, entry);
return entry;
}
notifyProbesEntryMissed(this, request);
} catch (Exception e) {
notifyProbesError(this, e);
// If an unexpected exception occurs, try to serve the page
// as if it wasn't in a cache.
LOGGER.log(Level.WARNING, LogMessages.WARNING_GRIZZLY_HTTP_SERVER_FILECACHE_GENERAL_ERROR(), e);
}
return null;
}
protected void remove(final FileCacheEntry entry) {
if (fileCacheMap.remove(entry.key) != null) {
cacheSize.decrementAndGet();
}
if (entry.type == FileCache.CacheType.MAPPED) {
subMappedMemorySize(entry.bb.remaining());
} else if (entry.type == FileCache.CacheType.HEAP) {
subHeapSize(entry.bb.remaining());
}
notifyProbesEntryRemoved(this, entry);
}
protected Object createJmxManagementObject() {
return MonitoringUtils.loadJmxObject("org.glassfish.grizzly.http.server.filecache.jmx.FileCache", this, FileCache.class);
}
Creates FileCacheEntry
. /**
* Creates {@link FileCacheEntry}.
*/
private FileCacheEntry createEntry(final File file) {
FileCacheEntry entry = tryMapFileToBuffer(file);
if (entry == null) {
entry = new FileCacheEntry(this);
entry.type = CacheType.FILE;
}
entry.plainFile = file;
entry.plainFileSize = file.length();
return entry;
}
Map the file to a ByteBuffer
Returns: the preinitialized FileCacheEntry
/**
* Map the file to a {@link ByteBuffer}
*
* @return the preinitialized {@link FileCacheEntry}
*/
private FileCacheEntry tryMapFileToBuffer(final File file) {
final long size = file.length();
if (size > getMaxEntrySize()) {
return null;
}
final CacheType type;
final ByteBuffer bb;
FileChannel fileChannel = null;
FileInputStream stream = null;
try {
if (size > getMinEntrySize()) {
if (addMappedMemorySize(size) > getMaxLargeFileCacheSize()) {
// Cache full
subMappedMemorySize(size);
return null;
}
type = CacheType.MAPPED;
} else {
if (addHeapSize(size) > getMaxSmallFileCacheSize()) {
// Cache full
subHeapSize(size);
return null;
}
type = CacheType.HEAP;
}
stream = new FileInputStream(file);
fileChannel = stream.getChannel();
bb = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
if (type == CacheType.HEAP) {
((MappedByteBuffer) bb).load();
}
} catch (Exception e) {
notifyProbesError(this, e);
return null;
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException ignored) {
notifyProbesError(this, ignored);
}
}
if (fileChannel != null) {
try {
fileChannel.close();
} catch (IOException ignored) {
notifyProbesError(this, ignored);
}
}
}
final FileCacheEntry entry = new FileCacheEntry(this);
entry.type = type;
entry.plainFileSize = size;
entry.bb = bb;
return entry;
}
Checks if the File
with the given content-type could be compressed. /**
* Checks if the {@link File} with the given content-type could be compressed.
*/
private boolean canBeCompressed(final File cacheFile, final String contentType) {
switch (compressionConfig.getCompressionMode()) {
case FORCE:
return true;
case OFF:
return false;
case ON: {
if (cacheFile.length() < compressionConfig.getCompressionMinSize()) {
return false;
}
return compressionConfig.checkMimeType(contentType);
}
default:
throw new IllegalStateException("Unknown mode");
}
}
// ------------------------------------------------ Configuration Properties
Returns: the maximum time, in seconds, a file may be cached.
/**
* @return the maximum time, in seconds, a file may be cached.
*/
public int getSecondsMaxAge() {
return secondsMaxAge;
}
Sets the maximum time, in seconds, a file may be cached.
Params: - secondsMaxAge – max age of a cached file, in seconds.
/**
* Sets the maximum time, in seconds, a file may be cached.
*
* @param secondsMaxAge max age of a cached file, in seconds.
*/
public void setSecondsMaxAge(int secondsMaxAge) {
this.secondsMaxAge = secondsMaxAge;
}
Returns: the maximum number of files that may be cached.
/**
* @return the maximum number of files that may be cached.
*/
public int getMaxCacheEntries() {
return maxCacheEntries;
}
Sets the maximum number of files that may be cached.
Params: - maxCacheEntries – the maximum number of files that may be cached.
/**
* Sets the maximum number of files that may be cached.
*
* @param maxCacheEntries the maximum number of files that may be cached.
*/
public void setMaxCacheEntries(int maxCacheEntries) {
this.maxCacheEntries = maxCacheEntries;
}
Returns: the minimum size, in bytes, a file must be in order to be cached in the heap cache.
/**
* @return the minimum size, in bytes, a file must be in order to be cached in the heap cache.
*/
public long getMinEntrySize() {
return minEntrySize;
}
The maximum size, in bytes, a file must be in order to be cached in the heap cache.
Params: - minEntrySize – the maximum size, in bytes, a file must be in order to be cached in the heap cache.
/**
* The maximum size, in bytes, a file must be in order to be cached in the heap cache.
*
* @param minEntrySize the maximum size, in bytes, a file must be in order to be cached in the heap cache.
*/
public void setMinEntrySize(long minEntrySize) {
this.minEntrySize = minEntrySize;
}
Returns: the maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
/**
* @return the maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
*/
public long getMaxEntrySize() {
return maxEntrySize;
}
The maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
Params: - maxEntrySize – the maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
/**
* The maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
*
* @param maxEntrySize the maximum size, in bytes, a resource may be before it can no longer be considered cacheable.
*/
public void setMaxEntrySize(long maxEntrySize) {
this.maxEntrySize = maxEntrySize;
}
Returns: the maximum size of the memory mapped cache for large files.
/**
* @return the maximum size of the memory mapped cache for large files.
*/
public long getMaxLargeFileCacheSize() {
return maxLargeFileCacheSize;
}
Sets the maximum size, in bytes, of the memory mapped cache for large files.
Params: - maxLargeFileCacheSize – the maximum size, in bytes, of the memory mapped cache for large files.
/**
* Sets the maximum size, in bytes, of the memory mapped cache for large files.
*
* @param maxLargeFileCacheSize the maximum size, in bytes, of the memory mapped cache for large files.
*/
public void setMaxLargeFileCacheSize(long maxLargeFileCacheSize) {
this.maxLargeFileCacheSize = maxLargeFileCacheSize;
}
Returns: the maximum size, in bytes, of the heap cache for files below the water mark set by getMinEntrySize()
.
/**
* @return the maximum size, in bytes, of the heap cache for files below the water mark set by
* {@link #getMinEntrySize()}.
*/
public long getMaxSmallFileCacheSize() {
return maxSmallFileCacheSize;
}
The maximum size, in bytes, of the heap cache for files below the water mark set by getMinEntrySize()
. Params: - maxSmallFileCacheSize – the maximum size, in bytes, of the heap cache for files below the water mark set by
getMinEntrySize()
.
/**
* The maximum size, in bytes, of the heap cache for files below the water mark set by {@link #getMinEntrySize()}.
*
* @param maxSmallFileCacheSize the maximum size, in bytes, of the heap cache for files below the water mark set by
* {@link #getMinEntrySize()}.
*/
public void setMaxSmallFileCacheSize(long maxSmallFileCacheSize) {
this.maxSmallFileCacheSize = maxSmallFileCacheSize;
}
Returns: true
if the FileCache
is enabled, otherwise false
/**
* @return <code>true</code> if the {@link FileCache} is enabled, otherwise <code>false</code>
*/
public boolean isEnabled() {
return enabled;
}
Params: - enabled –
true
to enable the FileCache
.
/**
* Enables/disables the {@link FileCache}. By default, the {@link FileCache} is disabled.
*
* @param enabled <code>true</code> to enable the {@link FileCache}.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
Returns the FileCache compression configuration settings.
/**
* Returns the <tt>FileCache</tt> compression configuration settings.
*/
public CompressionConfig getCompressionConfig() {
return compressionConfig;
}
Returns the folder to be used to store temporary compressed files.
/**
* Returns the folder to be used to store temporary compressed files.
*/
public File getCompressedFilesFolder() {
return compressedFilesFolder;
}
Sets the folder to be used to store temporary compressed files.
/**
* Sets the folder to be used to store temporary compressed files.
*/
public void setCompressedFilesFolder(final File compressedFilesFolder) {
this.compressedFilesFolder = compressedFilesFolder != null ? compressedFilesFolder : TMP_DIR;
}
Returns true
if File resources may be be sent using FileChannel.transferTo(long, long, WritableByteChannel)
.
By default, this property will be true, except in the following cases:
- JVM OS is HP-UX
- JVM OS is Linux, and the Oracle JVM in use is 1.6.0_17 or older
Finally, if the connection between endpoints is secure, send file functionality will be disabled regardless of
configuration.
Returns: true
if resources will be sent using FileChannel.transferTo(long, long, WritableByteChannel)
.Since: 2.3.5
/**
* <p>
* Returns <code>true</code> if File resources may be be sent using
* {@link java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)}.
* </p>
* <p/>
* <p>
* By default, this property will be true, except in the following cases:
* </p>
* <p/>
* <ul>
* <li>JVM OS is HP-UX</li>
* <li>JVM OS is Linux, and the Oracle JVM in use is 1.6.0_17 or older</li>
* </ul>
* <p/>
* <p/>
* <p>
* Finally, if the connection between endpoints is secure, send file functionality will be disabled regardless of
* configuration.
* </p>
*
* @return <code>true</code> if resources will be sent using
* {@link java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)}.
* @since 2.3.5
*/
public boolean isFileSendEnabled() {
return fileSendEnabled;
}
Configure whether or send-file support will enabled which allows sending File
resources via FileChannel.transferTo(long, long, WritableByteChannel)
. If disabled, the more traditional byte[] copy will be used to send content. Params: - fileSendEnabled –
true
to enable FileChannel.transferTo(long, long, WritableByteChannel)
support.
Since: 2.3.5
/**
* Configure whether or send-file support will enabled which allows sending {@link java.io.File} resources via
* {@link java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)}. If disabled, the
* more traditional byte[] copy will be used to send content.
*
* @param fileSendEnabled <code>true</code> to enable
* {@link java.nio.channels.FileChannel#transferTo(long, long, java.nio.channels.WritableByteChannel)} support.
* @since 2.3.5
*/
public void setFileSendEnabled(boolean fileSendEnabled) {
this.fileSendEnabled = fileSendEnabled;
}
Creates a temporary compressed representation of the given cache entry.
/**
* Creates a temporary compressed representation of the given cache entry.
*/
protected void compressFile(final FileCacheEntry entry) {
try {
final File tmpCompressedFile = File.createTempFile(String.valueOf(entry.plainFile.hashCode()), ".tmpzip", compressedFilesFolder);
tmpCompressedFile.deleteOnExit();
InputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(entry.plainFile);
out = new GZIPOutputStream(new FileOutputStream(tmpCompressedFile));
final byte[] tmp = new byte[1024];
do {
final int readNow = in.read(tmp);
if (readNow == -1) {
break;
}
out.write(tmp, 0, readNow);
} while (true);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}
if (out != null) {
try {
out.close();
} catch (IOException ignored) {
}
}
}
final long size = tmpCompressedFile.length();
switch (entry.type) {
case HEAP:
case MAPPED: {
final FileInputStream cFis = new FileInputStream(tmpCompressedFile);
try {
final FileChannel cFileChannel = cFis.getChannel();
final MappedByteBuffer compressedBb = cFileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
if (entry.type == CacheType.HEAP) {
compressedBb.load();
}
entry.compressedBb = compressedBb;
} finally {
cFis.close();
}
break;
}
case FILE: {
break;
}
default:
throw new IllegalStateException("The type is not supported: " + entry.type);
}
entry.compressedFileSize = size;
entry.compressedFile = tmpCompressedFile;
} catch (IOException e) {
LOGGER.log(Level.FINE, "Can not compress file: " + entry.plainFile, e);
}
}
// ---------------------------------------------------- Monitoring --------//
protected final long addHeapSize(long size) {
return heapSize.addAndGet(size);
}
protected final long subHeapSize(long size) {
return heapSize.addAndGet(-size);
}
Return the heap space used for cache
Returns: heap size
/**
* Return the heap space used for cache
*
* @return heap size
*/
public long getHeapCacheSize() {
return heapSize.get();
}
protected final long addMappedMemorySize(long size) {
return mappedMemorySize.addAndGet(size);
}
protected final long subMappedMemorySize(long size) {
return mappedMemorySize.addAndGet(-size);
}
Return the size of Mapped memory used for caching
Returns: Mapped memory size
/**
* Return the size of Mapped memory used for caching
*
* @return Mapped memory size
*/
public long getMappedCacheSize() {
return mappedMemorySize.get();
}
Check if the conditions specified in the optional If headers are satisfied.
Returns: HttpStatus
if the decision has been made and the response status has been defined, or null
otherwise
/**
* Check if the conditions specified in the optional If headers are satisfied.
*
* @return {@link HttpStatus} if the decision has been made and the response status has been defined, or <tt>null</tt>
* otherwise
*/
private HttpStatus checkIfHeaders(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException {
HttpStatus httpStatus = checkIfMatch(entry, request);
if (httpStatus == null) {
httpStatus = checkIfModifiedSince(entry, request);
if (httpStatus == null) {
httpStatus = checkIfNoneMatch(entry, request);
if (httpStatus == null) {
httpStatus = checkIfUnmodifiedSince(entry, request);
}
}
}
return httpStatus;
}
Check if the if-modified-since condition is satisfied.
Returns: HttpStatus
if the decision has been made and the response status has been defined, or null
otherwise
/**
* Check if the if-modified-since condition is satisfied.
*
* @return {@link HttpStatus} if the decision has been made and the response status has been defined, or <tt>null</tt>
* otherwise
*/
private HttpStatus checkIfModifiedSince(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException {
try {
final String reqModified = request.getHeader(Header.IfModifiedSince);
if (reqModified != null) {
// optimization - assume the String value sent in the
// client's If-Modified-Since header is the same as what
// was originally sent
if (reqModified.equals(entry.lastModifiedHeader)) {
return HttpStatus.NOT_MODIFIED_304;
}
long headerValue = convertToLong(reqModified);
if (headerValue != -1) {
long lastModified = entry.lastModified;
// If an If-None-Match header has been specified,
// If-Modified-Since is ignored.
if (request.getHeader(Header.IfNoneMatch) == null && lastModified - headerValue <= 1000) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
return HttpStatus.NOT_MODIFIED_304;
}
}
}
} catch (IllegalArgumentException illegalArgument) {
notifyProbesError(this, illegalArgument);
}
return null;
}
Check if the if-none-match condition is satisfied.
Returns: HttpStatus
if the decision has been made and the response status has been defined, or null
otherwise
/**
* Check if the if-none-match condition is satisfied.
*
* @return {@link HttpStatus} if the decision has been made and the response status has been defined, or <tt>null</tt>
* otherwise
*/
private HttpStatus checkIfNoneMatch(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException {
String headerValue = request.getHeader(Header.IfNoneMatch);
if (headerValue != null) {
String eTag = entry.Etag;
boolean conditionSatisfied = false;
if (!headerValue.equals("*")) {
StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag)) {
conditionSatisfied = true;
}
}
} else {
conditionSatisfied = true;
}
if (conditionSatisfied) {
// For GET and HEAD, we should respond with
// 304 Not Modified.
// For every other method, 412 Precondition Failed is sent
// back.
final Method method = request.getMethod();
if (Method.GET.equals(method) || Method.HEAD.equals(method)) {
return HttpStatus.NOT_MODIFIED_304;
} else {
return HttpStatus.PRECONDITION_FAILED_412;
}
}
}
return null;
}
Check if the if-unmodified-since condition is satisfied.
Returns: HttpStatus
if the decision has been made and the response status has been defined, or null
otherwise
/**
* Check if the if-unmodified-since condition is satisfied.
*
* @return {@link HttpStatus} if the decision has been made and the response status has been defined, or <tt>null</tt>
* otherwise
*/
private HttpStatus checkIfUnmodifiedSince(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException {
try {
long lastModified = entry.lastModified;
String h = request.getHeader(Header.IfUnmodifiedSince);
if (h != null) {
// optimization - assume the String value sent in the
// client's If-Unmodified-Since header is the same as what
// was originally sent
if (h.equals(entry.lastModifiedHeader)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
return HttpStatus.PRECONDITION_FAILED_412;
}
long headerValue = convertToLong(h);
if (headerValue != -1) {
if (headerValue - lastModified <= 1000) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
return HttpStatus.PRECONDITION_FAILED_412;
}
}
}
} catch (IllegalArgumentException illegalArgument) {
notifyProbesError(this, illegalArgument);
}
return null;
}
Check if the if-match condition is satisfied.
Params: - request – The servlet request we are processing
- entry – the FileCacheEntry to validate
Returns: HttpStatus
if the decision has been made and the response status has been defined, or null
otherwise
/**
* Check if the if-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param entry the FileCacheEntry to validate
* @return {@link HttpStatus} if the decision has been made and the response status has been defined, or <tt>null</tt>
* otherwise
*/
private HttpStatus checkIfMatch(final FileCacheEntry entry, final HttpRequestPacket request) throws IOException {
String headerValue = request.getHeader(Header.IfMatch);
if (headerValue != null) {
if (headerValue.indexOf('*') == -1) {
String eTag = entry.Etag;
StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
boolean conditionSatisfied = false;
while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
String currentToken = commaTokenizer.nextToken();
if (currentToken.trim().equals(eTag)) {
conditionSatisfied = true;
}
}
// If none of the given ETags match, 412 Precondition failed is
// sent back
if (!conditionSatisfied) {
return HttpStatus.PRECONDITION_FAILED_412;
}
}
}
return null;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public MonitoringConfig<FileCacheProbe> getMonitoringConfig() {
return monitoringConfig;
}
Notify registered FileCacheProbe
s about the "entry added" event. Params: - fileCache – the FileCache event occurred on.
- entry – entry been added
/**
* Notify registered {@link FileCacheProbe}s about the "entry added" event.
*
* @param fileCache the <tt>FileCache</tt> event occurred on.
* @param entry entry been added
*/
protected static void notifyProbesEntryAdded(final FileCache fileCache, final FileCacheEntry entry) {
final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null) {
for (FileCacheProbe probe : probes) {
probe.onEntryAddedEvent(fileCache, entry);
}
}
}
Notify registered FileCacheProbe
s about the "entry removed" event. Params: - fileCache – the FileCache event occurred on.
- entry – entry been removed
/**
* Notify registered {@link FileCacheProbe}s about the "entry removed" event.
*
* @param fileCache the <tt>FileCache</tt> event occurred on.
* @param entry entry been removed
*/
protected static void notifyProbesEntryRemoved(final FileCache fileCache, final FileCacheEntry entry) {
final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null) {
for (FileCacheProbe probe : probes) {
probe.onEntryRemovedEvent(fileCache, entry);
}
}
}
Notify registered FileCacheProbe
s about the "entry hit event. Params: - fileCache – the FileCache event occurred on.
- entry – entry been hit.
/**
* Notify registered {@link FileCacheProbe}s about the "entry hit event.
*
* @param fileCache the <tt>FileCache</tt> event occurred on.
* @param entry entry been hit.
*/
protected static void notifyProbesEntryHit(final FileCache fileCache, final FileCacheEntry entry) {
final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null) {
for (FileCacheProbe probe : probes) {
probe.onEntryHitEvent(fileCache, entry);
}
}
}
Notify registered FileCacheProbe
s about the "entry missed" event. Params: - fileCache – the FileCache event occurred on.
- request – HTTP request.
/**
* Notify registered {@link FileCacheProbe}s about the "entry missed" event.
*
* @param fileCache the <tt>FileCache</tt> event occurred on.
* @param request HTTP request.
*/
protected static void notifyProbesEntryMissed(final FileCache fileCache, final HttpRequestPacket request) {
final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null && probes.length > 0) {
for (FileCacheProbe probe : probes) {
probe.onEntryMissedEvent(fileCache, request.getHeader(Header.Host), request.getRequestURI());
}
}
}
Notify registered FileCacheProbe
s about the error. Params: - fileCache – the FileCache event occurred on.
/**
* Notify registered {@link FileCacheProbe}s about the error.
*
* @param fileCache the <tt>FileCache</tt> event occurred on.
*/
protected static void notifyProbesError(final FileCache fileCache, final Throwable error) {
final FileCacheProbe[] probes = fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null) {
for (FileCacheProbe probe : probes) {
probe.onErrorEvent(fileCache, error);
}
}
}
protected static long convertToLong(final String dateHeader) {
if (dateHeader == null) {
return -1L;
}
final SimpleDateFormats formats = SimpleDateFormats.create();
try {
// Attempt to convert the date header in a variety of formats
long result = FastHttpDateFormat.parseDate(dateHeader, formats.getFormats());
if (result != -1L) {
return result;
}
throw new IllegalArgumentException(dateHeader);
} finally {
formats.recycle();
}
}
private static class EntryWorker implements DelayedExecutor.Worker<FileCacheEntry> {
@Override
public boolean doWork(final FileCacheEntry element) {
element.run();
return true;
}
}
private static class EntryResolver implements DelayedExecutor.Resolver<FileCacheEntry> {
@Override
public boolean removeTimeout(FileCacheEntry element) {
if (element.timeoutMillis != -1) {
element.timeoutMillis = -1;
return true;
}
return false;
}
@Override
public long getTimeoutMillis(FileCacheEntry element) {
return element.timeoutMillis;
}
@Override
public void setTimeoutMillis(FileCacheEntry element, long timeoutMillis) {
element.timeoutMillis = timeoutMillis;
}
}
}