blob: cfdec4b1d3cf5423ac6aa384216a379d22818cca [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.GzipHttpContent;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MimeTypes.Type;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http.ResourceHttpContent;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
// TODO rename to ContentCache
public class ResourceCache implements HttpContent.Factory
{
private static final Logger LOG = Log.getLogger(ResourceCache.class);
private final ConcurrentMap<String,CachedHttpContent> _cache;
private final AtomicInteger _cachedSize;
private final AtomicInteger _cachedFiles;
private final ResourceFactory _factory;
private final ResourceCache _parent;
private final MimeTypes _mimeTypes;
private final boolean _etags;
private final boolean _gzip;
private final boolean _useFileMappedBuffer;
private int _maxCachedFileSize =128*1024*1024;
private int _maxCachedFiles=2048;
private int _maxCacheSize =256*1024*1024;
/* ------------------------------------------------------------ */
/** Constructor.
* @param parent the parent resource cache
* @param factory the resource factory
* @param mimeTypes Mimetype to use for meta data
* @param useFileMappedBuffer true to file memory mapped buffers
* @param etags true to support etags
* @param gzip true to support gzip
*/
public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags,boolean gzip)
{
_factory = factory;
_cache=new ConcurrentHashMap<String,CachedHttpContent>();
_cachedSize=new AtomicInteger();
_cachedFiles=new AtomicInteger();
_mimeTypes=mimeTypes;
_parent=parent;
_useFileMappedBuffer=useFileMappedBuffer;
_etags=etags;
_gzip=gzip;
}
/* ------------------------------------------------------------ */
public int getCachedSize()
{
return _cachedSize.get();
}
/* ------------------------------------------------------------ */
public int getCachedFiles()
{
return _cachedFiles.get();
}
/* ------------------------------------------------------------ */
public int getMaxCachedFileSize()
{
return _maxCachedFileSize;
}
/* ------------------------------------------------------------ */
public void setMaxCachedFileSize(int maxCachedFileSize)
{
_maxCachedFileSize = maxCachedFileSize;
shrinkCache();
}
/* ------------------------------------------------------------ */
public int getMaxCacheSize()
{
return _maxCacheSize;
}
/* ------------------------------------------------------------ */
public void setMaxCacheSize(int maxCacheSize)
{
_maxCacheSize = maxCacheSize;
shrinkCache();
}
/* ------------------------------------------------------------ */
/**
* @return Returns the maxCachedFiles.
*/
public int getMaxCachedFiles()
{
return _maxCachedFiles;
}
/* ------------------------------------------------------------ */
/**
* @param maxCachedFiles The maxCachedFiles to set.
*/
public void setMaxCachedFiles(int maxCachedFiles)
{
_maxCachedFiles = maxCachedFiles;
shrinkCache();
}
/* ------------------------------------------------------------ */
public boolean isUseFileMappedBuffer()
{
return _useFileMappedBuffer;
}
/* ------------------------------------------------------------ */
public void flushCache()
{
if (_cache!=null)
{
while (_cache.size()>0)
{
for (String path : _cache.keySet())
{
CachedHttpContent content = _cache.remove(path);
if (content!=null)
content.invalidate();
}
}
}
}
/* ------------------------------------------------------------ */
@Deprecated
public HttpContent lookup(String pathInContext)
throws IOException
{
return getContent(pathInContext,_maxCachedFileSize);
}
/* ------------------------------------------------------------ */
/** Get a Entry from the cache.
* Get either a valid entry object or create a new one if possible.
*
* @param pathInContext The key into the cache
* @param maxBufferSize The maximum buffer to allocated for this request. For cached content, a larger buffer may have
* previously been allocated and returned by the {@link HttpContent#getDirectBuffer()} or {@link HttpContent#getIndirectBuffer()} calls.
* @return The entry matching <code>pathInContext</code>, or a new entry
* if no matching entry was found. If the content exists but is not cachable,
* then a {@link ResourceHttpContent} instance is return. If
* the resource does not exist, then null is returned.
* @throws IOException Problem loading the resource
*/
@Override
public HttpContent getContent(String pathInContext,int maxBufferSize)
throws IOException
{
// Is the content in this cache?
CachedHttpContent content =_cache.get(pathInContext);
if (content!=null && (content).isValid())
return content;
// try loading the content from our factory.
Resource resource=_factory.getResource(pathInContext);
HttpContent loaded = load(pathInContext,resource,maxBufferSize);
if (loaded!=null)
return loaded;
// Is the content in the parent cache?
if (_parent!=null)
{
HttpContent httpContent=_parent.getContent(pathInContext,maxBufferSize);
if (httpContent!=null)
return httpContent;
}
return null;
}
/* ------------------------------------------------------------ */
/**
* @param resource the resource to test
* @return True if the resource is cacheable. The default implementation tests the cache sizes.
*/
protected boolean isCacheable(Resource resource)
{
if (_maxCachedFiles<=0)
return false;
long len = resource.length();
// Will it fit in the cache?
return (len>0 && (_useFileMappedBuffer || (len<_maxCachedFileSize && len<_maxCacheSize)));
}
/* ------------------------------------------------------------ */
private HttpContent load(String pathInContext, Resource resource, int maxBufferSize)
throws IOException
{
if (resource==null || !resource.exists())
return null;
if (resource.isDirectory())
return new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize());
// Will it fit in the cache?
if (isCacheable(resource))
{
CachedHttpContent content=null;
// Look for a gzip resource
if (_gzip)
{
String pathInContextGz=pathInContext+".gz";
CachedHttpContent contentGz = _cache.get(pathInContextGz);
if (contentGz==null || !contentGz.isValid())
{
contentGz=null;
Resource resourceGz=_factory.getResource(pathInContextGz);
if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()<resource.length())
{
contentGz = new CachedHttpContent(pathInContextGz,resourceGz,null);
CachedHttpContent added = _cache.putIfAbsent(pathInContextGz,contentGz);
if (added!=null)
{
contentGz.invalidate();
contentGz=added;
}
}
}
content = new CachedHttpContent(pathInContext,resource,contentGz);
}
else
content = new CachedHttpContent(pathInContext,resource,null);
// Add it to the cache.
CachedHttpContent added = _cache.putIfAbsent(pathInContext,content);
if (added!=null)
{
content.invalidate();
content=added;
}
return content;
}
// Look for non Cacheable gzip resource or content
String mt = _mimeTypes.getMimeByExtension(pathInContext);
if (_gzip)
{
// Is the gzip content cached?
String pathInContextGz=pathInContext+".gz";
CachedHttpContent contentGz = _cache.get(pathInContextGz);
if (contentGz!=null && contentGz.isValid() && contentGz.getResource().lastModified()>=resource.lastModified())
return new ResourceHttpContent(resource,mt,maxBufferSize,contentGz);
// Is there a gzip resource?
Resource resourceGz=_factory.getResource(pathInContextGz);
if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()<resource.length())
return new ResourceHttpContent(resource,mt,maxBufferSize,
new ResourceHttpContent(resourceGz,_mimeTypes.getMimeByExtension(pathInContextGz),maxBufferSize));
}
return new ResourceHttpContent(resource,mt,maxBufferSize);
}
/* ------------------------------------------------------------ */
private void shrinkCache()
{
// While we need to shrink
while (_cache.size()>0 && (_cachedFiles.get()>_maxCachedFiles || _cachedSize.get()>_maxCacheSize))
{
// Scan the entire cache and generate an ordered list by last accessed time.
SortedSet<CachedHttpContent> sorted= new TreeSet<CachedHttpContent>(
new Comparator<CachedHttpContent>()
{
public int compare(CachedHttpContent c1, CachedHttpContent c2)
{
if (c1._lastAccessed<c2._lastAccessed)
return -1;
if (c1._lastAccessed>c2._lastAccessed)
return 1;
if (c1._contentLengthValue<c2._contentLengthValue)
return -1;
return c1._key.compareTo(c2._key);
}
});
for (CachedHttpContent content : _cache.values())
sorted.add(content);
// Invalidate least recently used first
for (CachedHttpContent content : sorted)
{
if (_cachedFiles.get()<=_maxCachedFiles && _cachedSize.get()<=_maxCacheSize)
break;
if (content==_cache.remove(content.getKey()))
content.invalidate();
}
}
}
/* ------------------------------------------------------------ */
protected ByteBuffer getIndirectBuffer(Resource resource)
{
try
{
return BufferUtil.toBuffer(resource,true);
}
catch(IOException|IllegalArgumentException e)
{
LOG.warn(e);
return null;
}
}
/* ------------------------------------------------------------ */
protected ByteBuffer getMappedBuffer(Resource resource)
{
// Only use file mapped buffers for cached resources, otherwise too much virtual memory commitment for
// a non shared resource. Also ignore max buffer size
try
{
if (_useFileMappedBuffer && resource.getFile()!=null && resource.length()<Integer.MAX_VALUE)
return BufferUtil.toMappedBuffer(resource.getFile());
}
catch(IOException|IllegalArgumentException e)
{
LOG.warn(e);
}
return null;
}
/* ------------------------------------------------------------ */
protected ByteBuffer getDirectBuffer(Resource resource)
{
try
{
return BufferUtil.toBuffer(resource,true);
}
catch(IOException|IllegalArgumentException e)
{
LOG.warn(e);
}
return null;
}
/* ------------------------------------------------------------ */
@Override
public String toString()
{
return "ResourceCache["+_parent+","+_factory+"]@"+hashCode();
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/** MetaData associated with a context Resource.
*/
public class CachedHttpContent implements HttpContent
{
final String _key;
final Resource _resource;
final int _contentLengthValue;
final HttpField _contentType;
final String _characterEncoding;
final MimeTypes.Type _mimeType;
final HttpField _contentLength;
final HttpField _lastModified;
final long _lastModifiedValue;
final HttpField _etag;
final CachedGzipHttpContent _gzipped;
volatile long _lastAccessed;
AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
/* ------------------------------------------------------------ */
CachedHttpContent(String pathInContext,Resource resource,CachedHttpContent gzipped)
{
_key=pathInContext;
_resource=resource;
String contentType = _mimeTypes.getMimeByExtension(_resource.toString());
_contentType=contentType==null?null:new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,contentType);
_characterEncoding = _contentType==null?null:MimeTypes.getCharsetFromContentType(contentType);
_mimeType = _contentType==null?null:MimeTypes.CACHE.get(MimeTypes.getContentTypeWithoutCharset(contentType));
boolean exists=resource.exists();
_lastModifiedValue=exists?resource.lastModified():-1L;
_lastModified=_lastModifiedValue==-1?null
:new PreEncodedHttpField(HttpHeader.LAST_MODIFIED,DateGenerator.formatDate(_lastModifiedValue));
_contentLengthValue=exists?(int)resource.length():0;
_contentLength=new PreEncodedHttpField(HttpHeader.CONTENT_LENGTH,Long.toString(_contentLengthValue));
if (_cachedFiles.incrementAndGet()>_maxCachedFiles)
shrinkCache();
_lastAccessed=System.currentTimeMillis();
_etag=ResourceCache.this._etags?new PreEncodedHttpField(HttpHeader.ETAG,resource.getWeakETag()):null;
_gzipped=gzipped==null?null:new CachedGzipHttpContent(this,gzipped);
}
/* ------------------------------------------------------------ */
public String getKey()
{
return _key;
}
/* ------------------------------------------------------------ */
public boolean isCached()
{
return _key!=null;
}
/* ------------------------------------------------------------ */
public boolean isMiss()
{
return false;
}
/* ------------------------------------------------------------ */
@Override
public Resource getResource()
{
return _resource;
}
/* ------------------------------------------------------------ */
@Override
public HttpField getETag()
{
return _etag;
}
/* ------------------------------------------------------------ */
@Override
public String getETagValue()
{
return _etag.getValue();
}
/* ------------------------------------------------------------ */
boolean isValid()
{
if (_lastModifiedValue==_resource.lastModified() && _contentLengthValue==_resource.length())
{
_lastAccessed=System.currentTimeMillis();
return true;
}
if (this==_cache.remove(_key))
invalidate();
return false;
}
/* ------------------------------------------------------------ */
protected void invalidate()
{
ByteBuffer indirect=_indirectBuffer.get();
if (indirect!=null && _indirectBuffer.compareAndSet(indirect,null))
_cachedSize.addAndGet(-BufferUtil.length(indirect));
ByteBuffer direct=_directBuffer.get();
if (direct!=null && !BufferUtil.isMappedBuffer(direct) && _directBuffer.compareAndSet(direct,null))
_cachedSize.addAndGet(-BufferUtil.length(direct));
_cachedFiles.decrementAndGet();
_resource.close();
}
/* ------------------------------------------------------------ */
@Override
public HttpField getLastModified()
{
return _lastModified;
}
/* ------------------------------------------------------------ */
@Override
public String getLastModifiedValue()
{
return _lastModified==null?null:_lastModified.getValue();
}
/* ------------------------------------------------------------ */
@Override
public HttpField getContentType()
{
return _contentType;
}
/* ------------------------------------------------------------ */
@Override
public String getContentTypeValue()
{
return _contentType==null?null:_contentType.getValue();
}
/* ------------------------------------------------------------ */
@Override
public HttpField getContentEncoding()
{
return null;
}
/* ------------------------------------------------------------ */
@Override
public String getContentEncodingValue()
{
return null;
}
/* ------------------------------------------------------------ */
@Override
public String getCharacterEncoding()
{
return _characterEncoding;
}
/* ------------------------------------------------------------ */
@Override
public Type getMimeType()
{
return _mimeType;
}
/* ------------------------------------------------------------ */
@Override
public void release()
{
}
/* ------------------------------------------------------------ */
@Override
public ByteBuffer getIndirectBuffer()
{
ByteBuffer buffer = _indirectBuffer.get();
if (buffer==null)
{
ByteBuffer buffer2=ResourceCache.this.getIndirectBuffer(_resource);
if (buffer2==null)
LOG.warn("Could not load "+this);
else if (_indirectBuffer.compareAndSet(null,buffer2))
{
buffer=buffer2;
if (_cachedSize.addAndGet(BufferUtil.length(buffer))>_maxCacheSize)
shrinkCache();
}
else
buffer=_indirectBuffer.get();
}
if (buffer==null)
return null;
return buffer.slice();
}
/* ------------------------------------------------------------ */
@Override
public ByteBuffer getDirectBuffer()
{
ByteBuffer buffer = _directBuffer.get();
if (buffer==null)
{
ByteBuffer mapped = ResourceCache.this.getMappedBuffer(_resource);
ByteBuffer direct = mapped==null?ResourceCache.this.getDirectBuffer(_resource):mapped;
if (direct==null)
LOG.warn("Could not load "+this);
else if (_directBuffer.compareAndSet(null,direct))
{
buffer=direct;
if (mapped==null && _cachedSize.addAndGet(BufferUtil.length(buffer))>_maxCacheSize)
shrinkCache();
}
else
buffer=_directBuffer.get();
}
if (buffer==null)
return null;
return buffer.asReadOnlyBuffer();
}
/* ------------------------------------------------------------ */
@Override
public HttpField getContentLength()
{
return _contentLength;
}
/* ------------------------------------------------------------ */
@Override
public long getContentLengthValue()
{
return _contentLengthValue;
}
/* ------------------------------------------------------------ */
@Override
public InputStream getInputStream() throws IOException
{
ByteBuffer indirect = getIndirectBuffer();
if (indirect!=null && indirect.hasArray())
return new ByteArrayInputStream(indirect.array(),indirect.arrayOffset()+indirect.position(),indirect.remaining());
return _resource.getInputStream();
}
/* ------------------------------------------------------------ */
@Override
public ReadableByteChannel getReadableByteChannel() throws IOException
{
return _resource.getReadableByteChannel();
}
/* ------------------------------------------------------------ */
@Override
public String toString()
{
return String.format("CachedContent@%x{r=%s,e=%b,lm=%s,ct=%s,gz=%b}",hashCode(),_resource,_resource.exists(),_lastModified,_contentType,_gzipped!=null);
}
/* ------------------------------------------------------------ */
@Override
public HttpContent getGzipContent()
{
return (_gzipped!=null && _gzipped.isValid())?_gzipped:null;
}
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
public class CachedGzipHttpContent extends GzipHttpContent
{
private final CachedHttpContent _content;
private final CachedHttpContent _contentGz;
private final HttpField _etag;
CachedGzipHttpContent(CachedHttpContent content, CachedHttpContent contentGz)
{
super(content,contentGz);
_content=content;
_contentGz=contentGz;
_etag=(ResourceCache.this._etags)?new PreEncodedHttpField(HttpHeader.ETAG,_content.getResource().getWeakETag("--gzip")):null;
}
public boolean isValid()
{
return _contentGz.isValid() && _content.isValid() && _content.getResource().lastModified() <= _contentGz.getResource().lastModified();
}
@Override
public HttpField getETag()
{
if (_etag!=null)
return _etag;
return super.getETag();
}
@Override
public String getETagValue()
{
if (_etag!=null)
return _etag.getValue();
return super.getETagValue();
}
@Override
public String toString()
{
return "Cached"+super.toString();
}
}
}