blob: 204d71e49a391c0149a098306f47084b8d7100c5 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2014 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.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.HttpContent;
import org.eclipse.jetty.http.HttpContent.ResourceAsHttpContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.View;
import org.eclipse.jetty.io.nio.DirectNIOBuffer;
import org.eclipse.jetty.io.nio.IndirectNIOBuffer;
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 _etags;
private boolean _useFileMappedBuffer=true;
private int _maxCachedFileSize =4*1024*1024;
private int _maxCachedFiles=2048;
private int _maxCacheSize =32*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;
_etags=etags;
_useFileMappedBuffer=useFileMappedBuffer;
}
/* ------------------------------------------------------------ */
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 setUseFileMappedBuffer(boolean useFileMappedBuffer)
{
_useFileMappedBuffer = 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(),_etags);
}
/* ------------------------------------------------------------ */
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 Buffer getIndirectBuffer(Resource resource)
{
try
{
int len=(int)resource.length();
if (len<0)
{
LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
return null;
}
Buffer buffer = new IndirectNIOBuffer(len);
InputStream is = resource.getInputStream();
buffer.readFrom(is,len);
is.close();
return buffer;
}
catch(IOException e)
{
LOG.warn(e);
return null;
}
}
/* ------------------------------------------------------------ */
protected Buffer getDirectBuffer(Resource resource)
{
try
{
if (_useFileMappedBuffer && resource.getFile()!=null)
return new DirectNIOBuffer(resource.getFile());
int len=(int)resource.length();
if (len<0)
{
LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
return null;
}
Buffer buffer = new DirectNIOBuffer(len);
InputStream is = resource.getInputStream();
buffer.readFrom(is,len);
is.close();
return buffer;
}
catch(IOException 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 Buffer _lastModifiedBytes;
final Buffer _contentType;
final Buffer _etagBuffer;
volatile long _lastAccessed;
AtomicReference<Buffer> _indirectBuffer=new AtomicReference<Buffer>();
AtomicReference<Buffer> _directBuffer=new AtomicReference<Buffer>();
/* ------------------------------------------------------------ */
Content(String pathInContext,Resource resource)
{
_key=pathInContext;
_resource=resource;
_contentType=_mimeTypes.getMimeByExtension(_resource.toString());
boolean exists=resource.exists();
_lastModified=exists?resource.lastModified():-1;
_lastModifiedBytes=_lastModified<0?null:new ByteArrayBuffer(HttpFields.formatDate(_lastModified));
_length=exists?(int)resource.length():0;
_cachedSize.addAndGet(_length);
_cachedFiles.incrementAndGet();
_lastAccessed=System.currentTimeMillis();
_etagBuffer=_etags?new ByteArrayBuffer(resource.getWeakETag()):null;
}
/* ------------------------------------------------------------ */
public String getKey()
{
return _key;
}
/* ------------------------------------------------------------ */
public boolean isCached()
{
return _key!=null;
}
/* ------------------------------------------------------------ */
public boolean isMiss()
{
return false;
}
/* ------------------------------------------------------------ */
public Resource getResource()
{
return _resource;
}
/* ------------------------------------------------------------ */
public Buffer getETag()
{
return _etagBuffer;
}
/* ------------------------------------------------------------ */
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.release();
}
/* ------------------------------------------------------------ */
public Buffer getLastModified()
{
return _lastModifiedBytes;
}
/* ------------------------------------------------------------ */
public Buffer getContentType()
{
return _contentType;
}
/* ------------------------------------------------------------ */
public void release()
{
// don't release while cached. Release when invalidated.
}
/* ------------------------------------------------------------ */
public Buffer getIndirectBuffer()
{
Buffer buffer = _indirectBuffer.get();
if (buffer==null)
{
Buffer 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 new View(buffer);
}
/* ------------------------------------------------------------ */
public Buffer getDirectBuffer()
{
Buffer buffer = _directBuffer.get();
if (buffer==null)
{
Buffer 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 new View(buffer);
}
/* ------------------------------------------------------------ */
public long getContentLength()
{
return _length;
}
/* ------------------------------------------------------------ */
public InputStream getInputStream() throws IOException
{
Buffer indirect = getIndirectBuffer();
if (indirect!=null && indirect.array()!=null)
return new ByteArrayInputStream(indirect.array(),indirect.getIndex(),indirect.length());
return _resource.getInputStream();
}
/* ------------------------------------------------------------ */
@Override
public String toString()
{
return String.format("%s %s %d %s %s",_resource,_resource.exists(),_resource.lastModified(),_contentType,_lastModifiedBytes);
}
}
}