// ======================================================================== // $Id: ResourceCache.java,v 1.13 2006/04/04 22:28:02 gregwilkins Exp $ // Copyright 2000-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package net.lightbody.bmp.proxy.jetty.http; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import net.lightbody.bmp.proxy.jetty.util.*; import org.apache.commons.logging.Log; import java.io.IOException; import java.io.Serializable; import java.util.*; /* ------------------------------------------------------------ */ /** * @version $Id: ResourceCache.java,v 1.13 2006/04/04 22:28:02 gregwilkins Exp $ * @author Greg Wilkins */ public class ResourceCache implements LifeCycle, Serializable { private static Log log = LogFactory.getLog(ResourceCache.class); /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ private final static Map __dftMimeMap = new HashMap(); private final static Map __encodings = new HashMap(); static { ResourceBundle mime = ResourceBundle.getBundle("net/lightbody/bmp/proxy/jetty/http/mime"); Enumeration i = mime.getKeys(); while(i.hasMoreElements()) { String ext = (String)i.nextElement(); __dftMimeMap.put(StringUtil.asciiToLowerCase(ext),mime.getString(ext)); } ResourceBundle encoding = ResourceBundle.getBundle("net/lightbody/bmp/proxy/jetty/http/encoding"); i = encoding.getKeys(); while(i.hasMoreElements()) { String type = (String)i.nextElement(); __encodings.put(type,encoding.getString(type)); } } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ // TODO - handle this // These attributes are serialized by WebApplicationContext, which needs // to be updated if you add to these private int _maxCachedFileSize =1*1024; private int _maxCacheSize =1*1024; /* ------------------------------------------------------------ */ private Resource _resourceBase; private Map _mimeMap; private Map _encodingMap; /* ------------------------------------------------------------ */ private transient boolean _started; protected transient Map _cache; protected transient int _cacheSize; protected transient CachedMetaData _mostRecentlyUsed; protected transient CachedMetaData _leastRecentlyUsed; /* ------------------------------------------------------------ */ /** Constructor. */ public ResourceCache() { _cache=new HashMap(); } /* ------------------------------------------------------------ */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); _cache=new HashMap(); } /* ------------------------------------------------------------ */ /** Set the Resource Base. * The base resource is the Resource to use as a relative base * for all context resources. The ResourceBase attribute is a * string version of the baseResource. * If a relative file is passed, it is converted to a file * URL based on the current working directory. * @return The file or URL to use as the base for all resources * within the context. */ public String getResourceBase() { if (_resourceBase==null) return null; return _resourceBase.toString(); } /* ------------------------------------------------------------ */ /** Set the Resource Base. * The base resource is the Resource to use as a relative base * for all context resources. The ResourceBase attribute is a * string version of the baseResource. * If a relative file is passed, it is converted to a file * URL based on the current working directory. * @param resourceBase A URL prefix or directory name. */ public void setResourceBase(String resourceBase) { try{ _resourceBase=Resource.newResource(resourceBase); if(log.isDebugEnabled())log.debug("resourceBase="+_resourceBase+" for "+this); } catch(IOException e) { log.debug(LogSupport.EXCEPTION,e); throw new IllegalArgumentException(resourceBase+":"+e.toString()); } } /* ------------------------------------------------------------ */ /** Get the base resource. * The base resource is the Resource to use as a relative base * for all context resources. The ResourceBase attribute is a * string version of the baseResource. * @return The resourceBase as a Resource instance */ public Resource getBaseResource() { return _resourceBase; } /* ------------------------------------------------------------ */ /** Set the base resource. * The base resource is the Resource to use as a relative base * for all context resources. The ResourceBase attribute is a * string version of the baseResource. * @param base The resourceBase as a Resource instance */ public void setBaseResource(Resource base) { _resourceBase=base; } /* ------------------------------------------------------------ */ public int getMaxCachedFileSize() { return _maxCachedFileSize; } /* ------------------------------------------------------------ */ public void setMaxCachedFileSize(int maxCachedFileSize) { _maxCachedFileSize = maxCachedFileSize; _cache.clear(); } /* ------------------------------------------------------------ */ public int getMaxCacheSize() { return _maxCacheSize; } /* ------------------------------------------------------------ */ public void setMaxCacheSize(int maxCacheSize) { _maxCacheSize = maxCacheSize; _cache.clear(); } /* ------------------------------------------------------------ */ public void flushCache() { _cache.clear(); System.gc(); } /* ------------------------------------------------------------ */ /** Get a resource from the context. * Cached Resources are returned if the resource fits within the LRU * cache. Directories may have CachedResources returned, but the * caller must use the CachedResource.setCachedData method to set the * formatted directory content. * * @param pathInContext * @return Resource * @exception IOException */ public Resource getResource(String pathInContext) throws IOException { if(log.isTraceEnabled())log.trace("getResource "+pathInContext); if (_resourceBase==null) return null; Resource resource=null; // Cache operations synchronized(_cache) { // Look for it in the cache CachedResource cached = (CachedResource)_cache.get(pathInContext); if (cached!=null) { if(log.isTraceEnabled())log.trace("CACHE HIT: "+cached); CachedMetaData cmd = (CachedMetaData)cached.getAssociate(); if (cmd!=null && cmd.isValid()) return cached; } // Make the resource resource=_resourceBase.addPath(_resourceBase.encode(pathInContext)); if(log.isTraceEnabled())log.trace("CACHE MISS: "+resource); if (resource==null) return null; // Check for file aliasing if (resource.getAlias()!=null) { log.warn("Alias request of '"+resource.getAlias()+ "' for '"+resource+"'"); return null; } // Is it an existing file? long len = resource.length(); if (resource.exists()) { // Is it badly named? if (!resource.isDirectory() && pathInContext.endsWith("/")) return null; // Guess directory length. if (resource.isDirectory()) { if (resource.list()!=null) len=resource.list().length*100; else len=0; } // Is it cacheable? if (len>0 && len<_maxCachedFileSize && len<_maxCacheSize) { int needed=_maxCacheSize-(int)len; while(_cacheSize>needed) _leastRecentlyUsed.invalidate(); cached=resource.cache(); if(log.isTraceEnabled())log.trace("CACHED: "+resource); new CachedMetaData(cached,pathInContext); return cached; } } } // Non cached response new ResourceMetaData(resource); return resource; } /* ------------------------------------------------------------ */ public synchronized Map getMimeMap() { return _mimeMap; } /* ------------------------------------------------------------ */ /** * Also sets the org.mortbay.http.mimeMap context attribute * @param mimeMap */ public void setMimeMap(Map mimeMap) { _mimeMap = mimeMap; } /* ------------------------------------------------------------ */ /** Get the MIME type by filename extension. * @param filename A file name * @return MIME type matching the longest dot extension of the * file name. */ public String getMimeByExtension(String filename) { String type=null; if (filename!=null) { int i=-1; while(type==null) { i=filename.indexOf(".",i+1); if (i<0 || i>=filename.length()) break; String ext=StringUtil.asciiToLowerCase(filename.substring(i+1)); if (_mimeMap!=null) type = (String)_mimeMap.get(ext); if (type==null) type=(String)__dftMimeMap.get(ext); } } if (type==null) { if (_mimeMap!=null) type=(String)_mimeMap.get("*"); if (type==null) type=(String)__dftMimeMap.get("*"); } return type; } /* ------------------------------------------------------------ */ /** Set a mime mapping * @param extension * @param type */ public void setMimeMapping(String extension,String type) { if (_mimeMap==null) _mimeMap=new HashMap(); _mimeMap.put(StringUtil.asciiToLowerCase(extension),type); } /* ------------------------------------------------------------ */ /** Get the map of mime type to char encoding. * @return Map of mime type to character encodings. */ public synchronized Map getEncodingMap() { if (_encodingMap==null) _encodingMap=Collections.unmodifiableMap(__encodings); return _encodingMap; } /* ------------------------------------------------------------ */ /** Set the map of mime type to char encoding. * Also sets the org.mortbay.http.encodingMap context attribute * @param encodingMap Map of mime type to character encodings. */ public void setEncodingMap(Map encodingMap) { _encodingMap = encodingMap; } /* ------------------------------------------------------------ */ /** Get char encoding by mime type. * @param type A mime type. * @return The prefered character encoding for that type if known. */ public String getEncodingByMimeType(String type) { String encoding =null; if (type!=null) encoding=(String)_encodingMap.get(type); return encoding; } /* ------------------------------------------------------------ */ /** Set the encoding that should be used for a mimeType. * @param mimeType * @param encoding */ public void setTypeEncoding(String mimeType,String encoding) { getEncodingMap().put(mimeType,encoding); } /* ------------------------------------------------------------ */ public synchronized void start() throws Exception { if (isStarted()) return; getMimeMap(); getEncodingMap(); _started=true; } /* ------------------------------------------------------------ */ public boolean isStarted() { return _started; } /* ------------------------------------------------------------ */ /** Stop the context. */ public void stop() throws InterruptedException { _started=false; _cache.clear(); } /* ------------------------------------------------------------ */ /** Destroy a context. * Destroy a context and remove it from the HttpServer. The * HttpContext must be stopped before it can be destroyed. */ public void destroy() { if (isStarted()) throw new IllegalStateException("Started"); setMimeMap(null); _encodingMap=null; } /* ------------------------------------------------------------ */ /** Get Resource MetaData. * @param resource * @return Meta data for the resource. */ public ResourceMetaData getResourceMetaData(Resource resource) { Object o=resource.getAssociate(); if (o instanceof ResourceMetaData) return (ResourceMetaData)o; return new ResourceMetaData(resource); } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /** MetaData associated with a context Resource. */ public class ResourceMetaData { protected String _name; protected Resource _resource; ResourceMetaData(Resource resource) { _resource=resource; _name=_resource.toString(); _resource.setAssociate(this); } public String getLength() { return Long.toString(_resource.length()); } public String getLastModified() { return HttpFields.formatDate(_resource.lastModified(),false); } public String getMimeType() { return getMimeByExtension(_name); } } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ private class CachedMetaData extends ResourceMetaData { String _lastModified; String _encoding; String _length; String _key; CachedResource _cached; CachedMetaData _prev; CachedMetaData _next; CachedMetaData(CachedResource resource, String pathInContext) { super(resource); _cached=resource; _length=super.getLength(); _lastModified=super.getLastModified(); _encoding=super.getMimeType(); _key=pathInContext; _next=_mostRecentlyUsed; _mostRecentlyUsed=this; if (_next!=null) _next._prev=this; _prev=null; if (_leastRecentlyUsed==null) _leastRecentlyUsed=this; _cache.put(_key,resource); _cacheSize+=_cached.length(); } public String getLength() { return _length; } public String getLastModified() { return _lastModified; } public String getMimeType() { return _encoding; } /* ------------------------------------------------------------ */ boolean isValid() throws IOException { if (_cached.isUptoDate()) { if (_mostRecentlyUsed!=this) { CachedMetaData tp = _prev; CachedMetaData tn = _next; _next=_mostRecentlyUsed; _mostRecentlyUsed=this; if (_next!=null) _next._prev=this; _prev=null; if (tp!=null) tp._next=tn; if (tn!=null) tn._prev=tp; if (_leastRecentlyUsed==this && tp!=null) _leastRecentlyUsed=tp; } return true; } invalidate(); return false; } public void invalidate() { // Invalidate it _cache.remove(_key); _cacheSize=_cacheSize-(int)_cached.length(); if (_mostRecentlyUsed==this) _mostRecentlyUsed=_next; else _prev._next=_next; if (_leastRecentlyUsed==this) _leastRecentlyUsed=_prev; else _next._prev=_prev; _prev=null; _next=null; } } }