/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and
 *     "Apache Jetspeed" must not be used to endorse or promote products
 *    derived from this software without prior written permission. For
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache" or
 *    "Apache Jetspeed", nor may "Apache" appear in their name, without
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.jetspeed.cache;

import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
import java.io.File;

import org.apache.turbine.util.Log;

/**
 * FileCache keeps a cache of files up-to-date with a most simple eviction policy.
 * The eviction policy will keep n items in the cache, and then start evicting
 * the items ordered-by least used first. The cache runs a thread to check for
 * both evictions and refreshes.
 *
 *  @author David S. Taylor <a href="mailto:taylor@apache.org">David Sean Taylor</a>
 *  @version $Id: FileCache.java,v 1.2 2002/11/14 06:58:38 taylor Exp $
 */

public class FileCache implements java.util.Comparator
{
    protected long scanRate = 300;  // every 5 minutes
    protected int maxSize = 100; // maximum of 100 items
    protected List listeners = new LinkedList();

    private FileCacheScanner scanner = null;
    private Map cache = null;

    /**
     * Default constructor. Use default values for scanReate and maxSize
     *
     */
    public FileCache()
    {
        cache = new HashMap();
        this.scanner = new FileCacheScanner();
        this.scanner.setDaemon(true);
    }

    /**
     * Set scanRate and maxSize
     *
     * @param scanRate how often in seconds to refresh and evict from the cache
     * @param maxSize the maximum allowed size of the cache before eviction starts
     */
    public FileCache(long scanRate, 
                     int maxSize)
    {
        cache = new HashMap();

        this.scanRate = scanRate;
        this.maxSize = maxSize;
        this.scanner = new FileCacheScanner();
        this.scanner.setDaemon(true);
    }

    /**
     * Set all parameters on the cache
     *
     * @param initialCapacity the initial size of the cache as passed to HashMap
     * @param loadFactor how full the hash table is allowed to get before increasing
     * @param scanRate how often in seconds to refresh and evict from the cache
     * @param maxSize the maximum allowed size of the cache before eviction starts
     */
    public FileCache(int initialCapacity, 
                     int loadFactor, 
                     long scanRate, 
                     int maxSize)
    {
        cache = new HashMap(initialCapacity, loadFactor);

        this.scanRate = scanRate;
        this.maxSize = maxSize;
        this.scanner = new FileCacheScanner();
        this.scanner.setDaemon(true);
    }

    /**
     * Set the new refresh scan rate on managed files.
     *
     * @param scanRate the new scan rate in seconds
     */
    public void setScanRate(long scanRate)
    {
        this.scanRate= scanRate;
    }

    /**
     * Get the refresh scan rate 
     *
     * @return the current refresh scan rate in seconds
     */
    public long getScanRate()
    {
        return scanRate;
    }

    /**
     * Set the new maximum size of the cache 
     *
     * @param maxSize the maximum size of the cache
     */
    public void setMaxSize(int maxSize)
    {
        this.maxSize = maxSize;
    }

    /**
     * Get the maximum size of the cache 
     *
     * @return the current maximum size of the cache
     */
    public int getMaxSize()
    {
        return maxSize;
    }

    /**
     * Gets an entry from the cache given a key
     *
     * @param key the key to look up the entry by
     * @return the entry
     */
    public FileCacheEntry get(String key)
    {
        return (FileCacheEntry) cache.get(key);
    }

    /**
     * Gets an entry from the cache given a key
     *
     * @param key the key to look up the entry by
     * @return the entry
     */
    public Object getDocument(String key)
    {
        FileCacheEntry entry = (FileCacheEntry) cache.get(key);
        if (entry != null)
        {
            return entry.getDocument();
        }
        return null;
    }

    /**
     * Puts a file entry in the file cache
     *
     * @param file The file to be put in the cache
     * @param document the cached document
     */
    public void put(File file, Object document)
        throws java.io.IOException
    {
        FileCacheEntry entry = new FileCacheEntry(file, document);
        cache.put(file.getCanonicalPath(), entry);
    }

    /**
     * Puts a file entry in the file cache
     *
     * @param path the full path name of the file
     * @param document the cached document
     */
    public void put(String key, Object document)
        throws java.io.IOException
    {
        File file = new File(key);
        FileCacheEntry entry = new FileCacheEntry(file, document);
        cache.put(file.getCanonicalPath(), entry);
    }

    /**
     * Removes a file entry from the file cache
     *
     * @param key the full path name of the file
     * @return the entry removed
     */
    public Object remove(String key)
    {
        return cache.remove(key);
    }


    /**
     * Add a File Cache Event Listener 
     *
     * @param listener the event listener
     */
    public void addListener(FileCacheEventListener listener)
    {
        listeners.add(listener);
    }

    /**
     * Start the file Scanner running at the current scan rate.
     *
     */
    public void startFileScanner()
    {
        try
        {

            this.scanner.start();
        }
        catch (java.lang.IllegalThreadStateException e)
        {
            Log.error("Exception starting scanner", e);
        }
    }

    /**
     * Stop the file Scanner 
     *
     */
    public void stopFileScanner()
    {
        this.scanner.setStopping(true);
    }

    /**
     * Evicts entries based on last accessed time stamp
     *
     */
    protected void evict()        
    {
        synchronized (cache)
        {
            if (this.getMaxSize() >= cache.size())
            {
                return;
            }
    
            List list = new LinkedList(cache.values());
            Collections.sort(list, this);
    
            int count = 0;
            int limit = cache.size() - this.getMaxSize();
    
            for (Iterator it = list.iterator(); it.hasNext(); )
            {
                if (count >= limit)
                {
                    break;
                }
    
                FileCacheEntry entry = (FileCacheEntry) it.next();
                String key = null;
                try
                {
                    key = entry.getFile().getCanonicalPath();
                }                    
                catch (java.io.IOException e)
                {
                    Log.error("Exception getting file path: ", e);
                }
                // notify that eviction will soon take place
                for (Iterator lit = this.listeners.iterator(); lit.hasNext(); )
                {
                    FileCacheEventListener listener = 
                        (FileCacheEventListener) lit.next();
                    listener.evict(entry);                                    
                }
                cache.remove(key);
    
                count++;
            }        
        }
    }

    /**
     * Comparator function for sorting by last accessed during eviction
     *
     */
    public int compare(Object o1, Object o2)
    {
        FileCacheEntry e1 = (FileCacheEntry)o1;
        FileCacheEntry e2 = (FileCacheEntry)o2;
        if (e1.getLastAccessed() < e2.getLastAccessed())
        {
            return -1;
        }
        else if (e1.getLastAccessed() == e2.getLastAccessed())
        {
            return 0;
        }
        return 1;
    }

    /**
     * inner class that runs as a thread to scan the cache for updates or evictions
     *
     */
    protected class FileCacheScanner extends Thread
    {
        private boolean stopping = false;

        public void setStopping(boolean flag)
        {
            this.stopping = flag;
        }

        /**
         * Run the file scanner thread
         *
         */
        public void run()
        {
            boolean done = false;
    
            try
            {
                while(!done)
                {
                    try
                    {
                        int count = 0;
                        for (Iterator it = FileCache.this.cache.values().iterator(); it.hasNext(); )
                        {
                            FileCacheEntry entry = (FileCacheEntry) it.next();
                            Date modified = new Date(entry.getFile().lastModified());
    
                            if (modified.after(entry.getLastModified()))
                            {                            
                                for (Iterator lit = FileCache.this.listeners.iterator(); lit.hasNext(); )
                                {
                                    FileCacheEventListener listener = 
                                        (FileCacheEventListener) lit.next();
                                    listener.refresh(entry);                                    
                                    entry.setLastModified(modified);
                                }
                            }
                            count++;
                        }
                        if (count > FileCache.this.getMaxSize())
                        {
                            FileCache.this.evict();
                        }
                    }
                    catch (Exception e)
                    {
                        Log.error("FileCache Scanner: Error in iteration...", e);
                    }
    
                    sleep(FileCache.this.getScanRate() * 1000);                

                    if (this.stopping)
                    {
                        this.stopping = false;
                        done = true;
                    }
                }
            }
            catch (InterruptedException e)
            {
                Log.error("FileCacheScanner: recieved interruption, exiting.", e);
            }
        }
    } // end inner class:  FileCacheScanner


    /**
     * get an iterator over the cache values
     *
     * @return iterator over the cache values
     */
    public Iterator getIterator()
    {
        return cache.values().iterator();
    }

    /**
      * get the size of the cache
      *
      * @return the size of the cache
      */
    public int getSize()
    {
        return cache.size();
    }
}

