// ========================================================================
// $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.mortbay.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.mortbay.log.LogFactory;
import org.mortbay.util.CachedResource;
import org.mortbay.util.LifeCycle;
import org.mortbay.util.LogSupport;
import org.mortbay.util.Resource;
import org.mortbay.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/mortbay/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/mortbay/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;
        }
    }
    

}
