blob: cdedb2a96dcd0b32e0522d0cf2a349e3a6c79e8b [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.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.HttpContent;
import org.eclipse.jetty.http.MimeTypes;
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;
/* ------------------------------------------------------------ */
/**
*
*/
public class ResourceCache
{
private static final Logger LOG = Log.getLogger(ResourceCache.class);
private final ConcurrentMap<String,Content> _cache;
private final AtomicInteger _cachedSize;
private final AtomicInteger _cachedFiles;
private final ResourceFactory _factory;
private final ResourceCache _parent;
private final MimeTypes _mimeTypes;
private final boolean _etagSupported;
private final boolean _useFileMappedBuffer;
private int _maxCachedFileSize =128*1024*1024;
private int _maxCachedFiles=2048;
private int _maxCacheSize =256*1024*1024;
/* ------------------------------------------------------------ */
/** Constructor.
* @param mimeTypes Mimetype to use for meta data
*/
public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags)
{
_factory = factory;
_cache=new ConcurrentHashMap<String,Content>();
_cachedSize=new AtomicInteger();
_cachedFiles=new AtomicInteger();
_mimeTypes=mimeTypes;
_parent=parent;
_useFileMappedBuffer=useFileMappedBuffer;
_etagSupported=etags;
}
/* ------------------------------------------------------------ */
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())
{
Content content = _cache.remove(path);
if (content!=null)
content.invalidate();
}
}
}
}
/* ------------------------------------------------------------ */
/** 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
* @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 ResourceAsHttpContent} instance is return. If
* the resource does not exist, then null is returned.
* @throws IOException Problem loading the resource
*/
public HttpContent lookup(String pathInContext)
throws IOException
{
// Is the content in this cache?
Content 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);
if (loaded!=null)
return loaded;
// Is the content in the parent cache?
if (_parent!=null)
{
HttpContent httpContent=_parent.lookup(pathInContext);
if (httpContent!=null)
return httpContent;
}
return null;
}
/* ------------------------------------------------------------ */
/**
* @param resource
* @return True if the resource is cacheable. The default implementation tests the cache sizes.
*/
protected boolean isCacheable(Resource resource)
{
long len = resource.length();
// Will it fit in the cache?
return (len>0 && len<_maxCachedFileSize && len<_maxCacheSize);
}
/* ------------------------------------------------------------ */
private HttpContent load(String pathInContext, Resource resource)
throws IOException
{
Content content=null;
if (resource==null || !resource.exists())
return null;
// Will it fit in the cache?
if (!resource.isDirectory() && isCacheable(resource))
{
// Create the Content (to increment the cache sizes before adding the content
content = new Content(pathInContext,resource);
// reduce the cache to an acceptable size.
shrinkCache();
// Add it to the cache.
Content added = _cache.putIfAbsent(pathInContext,content);
if (added!=null)
{
content.invalidate();
content=added;
}
return content;
}
return new HttpContent.ResourceAsHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize(),_etagSupported);
}
/* ------------------------------------------------------------ */
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<Content> sorted= new TreeSet<Content>(
new Comparator<Content>()
{
public int compare(Content c1, Content c2)
{
if (c1._lastAccessed<c2._lastAccessed)
return -1;
if (c1._lastAccessed>c2._lastAccessed)
return 1;
if (c1._length<c2._length)
return -1;
return c1._key.compareTo(c2._key);
}
});
for (Content content : _cache.values())
sorted.add(content);
// Invalidate least recently used first
for (Content 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 getDirectBuffer(Resource resource)
{
try
{
if (_useFileMappedBuffer && resource.getFile()!=null && resource.length()<Integer.MAX_VALUE)
return BufferUtil.toMappedBuffer(resource.getFile());
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 Content implements HttpContent
{
final Resource _resource;
final int _length;
final String _key;
final long _lastModified;
final ByteBuffer _lastModifiedBytes;
final ByteBuffer _contentType;
final String _etag;
volatile long _lastAccessed;
AtomicReference<ByteBuffer> _indirectBuffer=new AtomicReference<ByteBuffer>();
AtomicReference<ByteBuffer> _directBuffer=new AtomicReference<ByteBuffer>();
/* ------------------------------------------------------------ */
Content(String pathInContext,Resource resource)
{
_key=pathInContext;
_resource=resource;
String mimeType = _mimeTypes.getMimeByExtension(_resource.toString());
_contentType=(mimeType==null?null:BufferUtil.toBuffer(mimeType));
boolean exists=resource.exists();
_lastModified=exists?resource.lastModified():-1;
_lastModifiedBytes=_lastModified<0?null:BufferUtil.toBuffer(DateGenerator.formatDate(_lastModified));
_length=exists?(int)resource.length():0;
_cachedSize.addAndGet(_length);
_cachedFiles.incrementAndGet();
_lastAccessed=System.currentTimeMillis();
_etag=ResourceCache.this._etagSupported?resource.getWeakETag():null;
}
/* ------------------------------------------------------------ */
public String getKey()
{
return _key;
}
/* ------------------------------------------------------------ */
public boolean isCached()
{
return _key!=null;
}
/* ------------------------------------------------------------ */
public boolean isMiss()
{
return false;
}
/* ------------------------------------------------------------ */
@Override
public Resource getResource()
{
return _resource;
}
/* ------------------------------------------------------------ */
@Override
public String getETag()
{
return _etag;
}
/* ------------------------------------------------------------ */
boolean isValid()
{
if (_lastModified==_resource.lastModified() && _length==_resource.length())
{
_lastAccessed=System.currentTimeMillis();
return true;
}
if (this==_cache.remove(_key))
invalidate();
return false;
}
/* ------------------------------------------------------------ */
protected void invalidate()
{
// Invalidate it
_cachedSize.addAndGet(-_length);
_cachedFiles.decrementAndGet();
_resource.close();
}
/* ------------------------------------------------------------ */
@Override
public String getLastModified()
{
return BufferUtil.toString(_lastModifiedBytes);
}
/* ------------------------------------------------------------ */
@Override
public String getContentType()
{
return BufferUtil.toString(_contentType);
}
/* ------------------------------------------------------------ */
@Override
public void release()
{
// don't release while cached. Release when invalidated.
}
/* ------------------------------------------------------------ */
@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;
else
buffer=_indirectBuffer.get();
}
if (buffer==null)
return null;
return buffer.slice();
}
/* ------------------------------------------------------------ */
@Override
public ByteBuffer getDirectBuffer()
{
ByteBuffer buffer = _directBuffer.get();
if (buffer==null)
{
ByteBuffer buffer2=ResourceCache.this.getDirectBuffer(_resource);
if (buffer2==null)
LOG.warn("Could not load "+this);
else if (_directBuffer.compareAndSet(null,buffer2))
buffer=buffer2;
else
buffer=_directBuffer.get();
}
if (buffer==null)
return null;
return buffer.asReadOnlyBuffer();
}
/* ------------------------------------------------------------ */
@Override
public long getContentLength()
{
return _length;
}
/* ------------------------------------------------------------ */
@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}",hashCode(),_resource,_resource.exists(),BufferUtil.toString(_lastModifiedBytes),_contentType);
}
}
}