com.sun.grizzly.http.server.filecache.FileCache Maven / Gradle / Ivy
The newest version!
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.grizzly.http.server.filecache;
import com.sun.grizzly.Buffer;
import com.sun.grizzly.Grizzly;
import com.sun.grizzly.http.HttpContent;
import com.sun.grizzly.http.HttpPacket;
import com.sun.grizzly.http.HttpRequestPacket;
import com.sun.grizzly.http.HttpResponsePacket;
import com.sun.grizzly.http.util.BufferChunk;
import com.sun.grizzly.http.util.HttpStatus;
import com.sun.grizzly.http.util.MimeHeaders;
import com.sun.grizzly.memory.MemoryManager;
import com.sun.grizzly.memory.MemoryUtils;
import com.sun.grizzly.monitoring.jmx.AbstractJmxMonitoringConfig;
import com.sun.grizzly.monitoring.jmx.JmxMonitoringAware;
import com.sun.grizzly.monitoring.jmx.JmxMonitoringConfig;
import com.sun.grizzly.monitoring.jmx.JmxObject;
import com.sun.grizzly.utils.DelayedExecutor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
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.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class implements a file caching mechanism used to cache static resources.
*
* @author Jeanfrancois Arcand
* @author Scott Oaks
*/
public class FileCache implements JmxMonitoringAware {
public enum CacheType {
HEAP, MAPPED
}
private static final Logger logger = Grizzly.logger(FileCache.class);
/**
* A {@link ByteBuffer} cache of static pages.
*/
private final ConcurrentHashMap fileCacheMap =
new ConcurrentHashMap();
private final FileCacheEntry NULL_CACHE_ENTRY = new FileCacheEntry(this);
/**
* Specifies the maximum time in seconds a resource may be cached.
*/
private int secondsMaxAge = -1;
/**
* The maximum entries in the {@link FileCache}
*/
private volatile int maxCacheEntries = 1024;
/**
* The maximum size of a cached resource.
*/
private long minEntrySize = Long.MIN_VALUE;
/**
* The maximum size of a cached resource.
*/
private long maxEntrySize = Long.MAX_VALUE;
/**
* The maximum memory mapped bytes.
*/
private volatile long maxLargeFileCacheSize = Long.MAX_VALUE;
/**
* The maximum cached bytes
*/
private volatile long maxSmallFileCacheSize = 1048576;
/**
* The current cache size in bytes
*/
private AtomicLong mappedMemorySize = new AtomicLong();
/**
* The current cache size in bytes
*/
private AtomicLong heapSize = new AtomicLong();
/**
* Is the file cache enabled.
*/
private boolean enabled = true;
private MemoryManager memoryManager;
private DelayedExecutor.DelayQueue delayQueue;
/**
* File cache probes
*/
protected final AbstractJmxMonitoringConfig monitoringConfig =
new AbstractJmxMonitoringConfig(FileCacheProbe.class) {
@Override
public JmxObject createManagementObject() {
return createJmxManagementObject();
}
};
// ---------------------------------------------------- Methods ----------//
public void initialize(MemoryManager memoryManager,
DelayedExecutor delayedExecutor) {
this.memoryManager = memoryManager;
delayQueue = delayedExecutor.createDelayQueue(new EntryWorker(),
new EntryResolver());
}
/**
* Add a resource to the cache. Currently, only static resources served
* by the DefaultServlet can be cached.
*/
public void add(final HttpRequestPacket request, File cacheFile) {
final String requestURI = request.getRequestURI();
final String host = request.getHeader("Host");
final HttpResponsePacket response = request.getResponse();
final MimeHeaders headers = response.getHeaders();
final FileCacheKey key = new FileCacheKey(host, requestURI);
if (requestURI == null || fileCacheMap.putIfAbsent(key, NULL_CACHE_ENTRY) != null) {
//@TODO return key and entry to the thread cache object pool
return;
}
// cache is full.
if (fileCacheMap.size() > getMaxCacheEntries()) {
fileCacheMap.remove(key);
//@TODO return key and entry to the thread cache object pool
return;
}
final FileCacheEntry entry = mapFile(cacheFile);
// Always put the answer into the map. If it's null, then
// we know that it doesn't fit into the cache, so there's no
// reason to go through this code again.
if (entry == null) {
return;
}
entry.key = key;
entry.requestURI = requestURI;
String lastModified = headers.getHeader("Last-Modified");
entry.lastModified = (lastModified == null
? String.valueOf(cacheFile.lastModified()) : lastModified);
entry.contentType = headers.getHeader("Content-type");
entry.xPoweredBy = headers.getHeader("X-Powered-By");
entry.date = headers.getHeader("Date");
entry.Etag = headers.getHeader("Etag");
entry.contentLength = response.getContentLength();
entry.host = host;
fileCacheMap.put(key, entry);
notifyProbesEntryAdded(this, entry);
final int secondsMaxAgeLocal = getSecondsMaxAge();
if (secondsMaxAgeLocal > 0) {
delayQueue.add(entry, secondsMaxAgeLocal, TimeUnit.SECONDS);
}
}
/**
* Send the cache.
*/
public HttpPacket get(final HttpRequestPacket request) {
final String requestURI = request.getRequestURI();
final String host = request.getHeader("Host");
final FileCacheKey key = new FileCacheKey(host, requestURI);
final FileCacheEntry entry = fileCacheMap.get(key);
try {
if (entry != null && entry != NULL_CACHE_ENTRY) {
notifyProbesEntryHit(this, entry);
return makeResponse(entry, request);
} else {
notifyProbesEntryMissed(this, host, requestURI);
}
} 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, "File Cache exception", e);
}
return null;
}
final ConcurrentHashMap getFileCacheMap() {
return fileCacheMap;
}
protected void remove(FileCacheEntry entry) {
fileCacheMap.remove(entry.key);
if (entry.type == FileCache.CacheType.MAPPED) {
subMappedMemorySize(entry.bb.remaining());
} else {
subHeapSize(entry.bb.remaining());
}
notifyProbesEntryRemoved(this, entry);
}
protected JmxObject createJmxManagementObject() {
return new com.sun.grizzly.http.server.filecache.jmx.FileCache(this);
}
/**
* Map the file to a {@link ByteBuffer}
* @return the preinitialized {@link FileCacheEntry}
*/
private FileCacheEntry mapFile(File file) {
final CacheType type;
final long size;
final ByteBuffer bb;
FileChannel fileChannel = null;
FileInputStream stream = null;
try {
stream = new FileInputStream(file);
fileChannel = stream.getChannel();
size = fileChannel.size();
if (size > getMaxEntrySize()) {
return null;
}
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;
}
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.fileSize = size;
entry.bb = bb;
return entry;
}
// -------------------------------------------------- Static cache -------/
/**
* Send the cached resource.
*/
@SuppressWarnings({"unchecked"})
protected HttpPacket makeResponse(FileCacheEntry entry,
HttpRequestPacket request) throws IOException {
final HttpResponsePacket response = request.getResponse();
HttpStatus.OK_200.setValues(request.getResponse());
boolean flushBody = checkIfHeaders(entry, request);
response.setContentType(entry.contentType);
if (flushBody) {
response.setContentLength(entry.contentLength);
final ByteBuffer sliced = entry.bb.slice();
final Buffer buffer = MemoryUtils.wrap(memoryManager, sliced);
return HttpContent.builder(response)
.content(buffer)
.last(true)
.build();
} else {
response.setChunked(false);
}
return response;
}
// ------------------------------------------------ Configuration Properties
/**
* @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.
*
* @param secondsMaxAge max age of a cached file, in seconds.
*/
public void setSecondsMaxAge(int secondsMaxAge) {
this.secondsMaxAge = secondsMaxAge;
}
/**
* @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.
*
* @param maxCacheEntries the maximum number of files that may be cached.
*/
public void setMaxCacheEntries(int maxCacheEntries) {
this.maxCacheEntries = maxCacheEntries;
}
/**
* @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.
*
* @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;
}
/**
* @return the maximum size, in bytes, a resource may be before it can no
* longer be considered cachable.
*/
public long getMaxEntrySize() {
return maxEntrySize;
}
/**
* The maximum size, in bytes, a resource may be before it can no
* longer be considered cachable.
*
* @param maxEntrySize the maximum size, in bytes, a resource may be before it can no
* longer be considered cachable.
*/
public void setMaxEntrySize(long maxEntrySize) {
this.maxEntrySize = maxEntrySize;
}
/**
* @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.
*
* @param maxLargeFileCacheSize the maximum size, in bytes, of the memory
* mapped cache for large files.
*/
public void setMaxLargeFileCacheSize(long maxLargeFileCacheSize) {
this.maxLargeFileCacheSize = maxLargeFileCacheSize;
}
/**
* @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 {@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;
}
/**
* @return true if the {@link FileCache} is enabled,
* otherwise false
*/
public boolean isEnabled() {
return enabled;
}
/**
* Enables/disables the {@link FileCache}. By default, the
* {@link FileCache} is disabled.
*
* @param enabled true to enable the {@link FileCache}.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
// ---------------------------------------------------- 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
* @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
* @return Mapped memory size
*/
public long getMappedCacheSize() {
return mappedMemorySize.get();
}
/**
* Check if the conditions specified in the optional If headers are
* satisfied.
*
* @return boolean true if the resource meets all the specified conditions,
* and false if any of the conditions is not satisfied, in which case
* request processing is stopped
*/
protected boolean checkIfHeaders(final FileCacheEntry entry,
final HttpRequestPacket request) throws IOException {
return checkIfMatch(entry, request)
&& checkIfModifiedSince(entry, request)
&& checkIfNoneMatch(entry, request)
&& checkIfUnmodifiedSince(entry, request);
}
/**
* Check if the if-modified-since condition is satisfied.
*
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
private boolean checkIfModifiedSince(final FileCacheEntry entry,
final HttpRequestPacket request) throws IOException {
try {
HttpResponsePacket response = request.getResponse();
String h = request.getHeader("If-Modified-Since");
long headerValue = (h == null ? -1 : Long.parseLong(h));
if (headerValue != -1) {
long lastModified = Long.parseLong(entry.lastModified);
// If an If-None-Match header has been specified,
// If-Modified-Since is ignored.
if ((request.getHeader("If-None-Match") == null)
&& (lastModified < headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
HttpStatus.NOT_MODIFIED_304.setValues(response);
response.setHeader("ETag", getETag(entry));
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
notifyProbesError(this, illegalArgument);
return true;
}
return true;
}
/**
* Check if the if-none-match condition is satisfied.
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
private boolean checkIfNoneMatch(final FileCacheEntry entry,
final HttpRequestPacket request) throws IOException {
final HttpResponsePacket response = request.getResponse();
String headerValue = request.getHeader("If-None-Match");
if (headerValue != null) {
String eTag = getETag(entry);
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 BufferChunk methodBC = request.getMethodBC();
if (methodBC.equals("GET") || methodBC.equals("HEAD")) {
HttpStatus.NOT_MODIFIED_304.setValues(response);
response.setHeader("ETag", eTag);
return false;
} else {
HttpStatus.PRECONDITION_FAILED_412.setValues(response);
return false;
}
}
}
return true;
}
/**
* Check if the if-unmodified-since condition is satisfied.
*
* @return boolean true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfUnmodifiedSince(final FileCacheEntry entry,
final HttpRequestPacket request) throws IOException {
try {
final HttpResponsePacket response = request.getResponse();
long lastModified = Long.parseLong(entry.lastModified);
String h = request.getHeader("If-Unmodified-Since");
long headerValue = (h == null ? -1 : Long.parseLong(h));
if (headerValue != -1) {
if (lastModified >= (headerValue + 1000)) {
// The entity has not been modified since the date
// specified by the client. This is not an error case.
HttpStatus.PRECONDITION_FAILED_412.setValues(response);
return false;
}
}
} catch (IllegalArgumentException illegalArgument) {
notifyProbesError(this, illegalArgument);
return true;
}
return true;
}
/**
* Get ETag.
*
* @return strong ETag if available, else weak ETag
*/
private String getETag(FileCacheEntry entry) {
String result = entry.Etag;
if (result == null) {
final StringBuilder sb = new StringBuilder();
long contentLength = entry.fileSize;
long lastModified = Long.parseLong(entry.lastModified);
if ((contentLength >= 0) || (lastModified >= 0)) {
sb.append("W/\"").append(contentLength).append('-').
append(lastModified).append('"');
result = sb.toString();
entry.Etag = result;
}
}
return result;
}
/**
* Check if the if-match condition is satisfied.
*
* @param request The servlet request we are processing
* @param entry the FileCacheEntry to validate
* @return true if the resource meets the specified condition,
* and false if the condition is not satisfied, in which case request
* processing is stopped
*/
protected boolean checkIfMatch(final FileCacheEntry entry,
final HttpRequestPacket request) throws IOException {
HttpResponsePacket response = request.getResponse();
String headerValue = request.getHeader("If-Match");
if (headerValue != null) {
if (headerValue.indexOf('*') == -1) {
String eTag = getETag(entry);
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) {
HttpStatus.PRECONDITION_FAILED_412.setValues(response);
return false;
}
}
}
return true;
}
@Override
public JmxMonitoringConfig getMonitoringConfig() {
return monitoringConfig;
}
/**
* Notify registered {@link FileCacheProbe}s about the "entry added" event.
*
* @param fileCache the FileCache 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 {@link FileCacheProbe}s about the "entry removed" event.
*
* @param fileCache the FileCache 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 {@link FileCacheProbe}s about the "entry hitted" event.
*
* @param fileCache the FileCache event occurred on.
* @param entry entry been hitted.
*/
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 {@link FileCacheProbe}s about the "entry missed" event.
*
* @param fileCache the FileCache event occurred on.
* @param host requested HTTP "Host" parameter.
* @param requestURI requested HTTP request URL.
*/
protected static void notifyProbesEntryMissed(final FileCache fileCache,
final String host, final String requestURI) {
final FileCacheProbe[] probes =
fileCache.monitoringConfig.getProbesUnsafe();
if (probes != null) {
for (FileCacheProbe probe : probes) {
probe.onEntryMissedEvent(fileCache, host, requestURI);
}
}
}
/**
* Notify registered {@link FileCacheProbe}s about the error.
*
* @param fileCache the FileCache 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);
}
}
}
private class EntryWorker implements DelayedExecutor.Worker {
@Override
public void doWork(FileCacheEntry element) {
element.run();
}
}
private class EntryResolver implements DelayedExecutor.Resolver {
@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;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy