| // ======================================================================== |
| // $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 org.openqa.jetty.http; |
| |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.ResourceBundle; |
| |
| import org.apache.commons.logging.Log; |
| import org.openqa.jetty.log.LogFactory; |
| import org.openqa.jetty.util.CachedResource; |
| import org.openqa.jetty.util.LifeCycle; |
| import org.openqa.jetty.util.LogSupport; |
| import org.openqa.jetty.util.Resource; |
| import org.openqa.jetty.util.StringUtil; |
| |
| |
| /* ------------------------------------------------------------ */ |
| /** |
| * @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("org/openqa/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("org/openqa/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.openqa.jetty.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.openqa.jetty.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; |
| } |
| } |
| |
| |
| } |