package org.apache.turbine.services.cache;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.commons.configuration.Configuration;

import org.apache.turbine.services.InitializationException;
import org.apache.turbine.services.TurbineBaseService;

/**
 * This Service functions as a Global Cache.  A global cache is a good
 * place to store items that you may need to access often but don't
 * necessarily need (or want) to fetch from the database everytime.  A
 * good example would be a look up table of States that you store in a
 * database and use throughout your application.  Since information
 * about States doesn't change very often, you could store this
 * information in the Global Cache and decrease the overhead of
 * hitting the database everytime you need State information.
 *
 * The following properties are needed to configure this service:<br>
 *
 * <code><pre>
 * services.GlobalCacheService.classname=org.apache.turbine.services.cache.TurbineGlobalCacheService
 * services.GlobalCacheService.cache.initial.size=20
 * services.GlobalCacheService.cache.check.frequency=5000
 * </pre></code>
 *
 * <dl>
 * <dt>classname</dt><dd>the classname of this service</dd>
 * <dt>cache.initial.size</dt><dd>Initial size of hash table use to store cached
 objects.  If this property is not present, the default value is 20</dd>
 * <dt>cache.check.frequency</dt><dd>Cache check frequency in Millis (1000
 Millis = 1 second).  If this property is not present, the default value is 5000</dd>
 * </dl>
 *
 * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
 * @author <a href="mailto:jon@clearink.com">Jon S. Stevens</a>
 * @author <a href="mailto:john@zenplex.com">John Thorhauer</a>
 * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
 * @version $Id: TurbineGlobalCacheService.java 534527 2007-05-02 16:10:59Z tv $
 */
public class TurbineGlobalCacheService
        extends TurbineBaseService
        implements GlobalCacheService,
        Runnable
{
    /**
     * Initial size of hash table
     * Value must be > 0.
     * Default = 20
     */
    public static final int DEFAULT_INITIAL_CACHE_SIZE = 20;

    /**
     * The property for the InitalCacheSize
     */
    public static final String INITIAL_CACHE_SIZE = "cache.initial.size";

    /**
     * The property for the Cache check frequency
     */
    public static final String CACHE_CHECK_FREQUENCY = "cache.check.frequency";

    /**
     * Cache check frequency in Millis (1000 Millis = 1 second).
     * Value must be > 0.
     * Default = 5 seconds
     */
    public static final long DEFAULT_CACHE_CHECK_FREQUENCY = 5000; // 5 seconds

    /** The cache. **/
    private Hashtable cache = null;

    /** cacheCheckFrequency (default - 5 seconds) */
    private long cacheCheckFrequency = DEFAULT_CACHE_CHECK_FREQUENCY;

    /**
     * Constructor.
     */
    public TurbineGlobalCacheService()
    {
    }

    /**
     * Called the first time the Service is used.
     */
    public void init()
            throws InitializationException
    {
        int cacheInitialSize = DEFAULT_INITIAL_CACHE_SIZE;
        Configuration conf = getConfiguration();
        if (conf != null)
        {
            try
            {
                cacheInitialSize = conf.getInt(INITIAL_CACHE_SIZE, DEFAULT_INITIAL_CACHE_SIZE);
                if (cacheInitialSize <= 0)
                {
                    throw new IllegalArgumentException(INITIAL_CACHE_SIZE + " must be >0");
                }
                cacheCheckFrequency = conf.getLong(CACHE_CHECK_FREQUENCY, DEFAULT_CACHE_CHECK_FREQUENCY);
                if (cacheCheckFrequency <= 0)
                {
                    throw new IllegalArgumentException(CACHE_CHECK_FREQUENCY + " must be >0");
                }
            }
            catch (Exception x)
            {
                throw new InitializationException(
                        "Failed to initialize TurbineGlobalCacheService", x);
            }
        }

        try
        {
            cache = new Hashtable(cacheInitialSize);

            // Start housekeeping thread.
            Thread housekeeping = new Thread(this);
            // Indicate that this is a system thread. JVM will quit only when there
            // are no more active user threads. Settings threads spawned internally
            // by Turbine as daemons allows commandline applications using Turbine
            // to terminate in an orderly manner.
            housekeeping.setDaemon(true);
            housekeeping.start();

            setInit(true);
        }
        catch (Exception e)
        {
            throw new InitializationException(
                    "TurbineGlobalCacheService failed to initialize", e);
        }
    }

    /**
     * Returns an item from the cache.  RefreshableCachedObject will be
     * refreshed if it is expired and not untouched.
     *
     * @param id The key of the stored object.
     * @return The object from the cache.
     * @exception ObjectExpiredException when either the object is
     * not in the cache or it has expired.
     */
    public CachedObject getObject(String id)
            throws ObjectExpiredException
    {
        CachedObject obj = null;

        obj = (CachedObject) cache.get(id);

        if (obj == null)
        {
            // Not in the cache.
            throw new ObjectExpiredException();
        }

        if (obj.isStale())
        {
            if (obj instanceof RefreshableCachedObject)
            {
                RefreshableCachedObject rco = (RefreshableCachedObject) obj;
                if (rco.isUntouched())
                // Do not refresh an object that has exceeded TimeToLive
                    throw new ObjectExpiredException();
                // Refresh Object
                rco.refresh();
                if (rco.isStale())
                // Object is Expired.
                    throw new ObjectExpiredException();
            }
            else
            {
                // Expired.
                throw new ObjectExpiredException();
            }
        }

        if (obj instanceof RefreshableCachedObject)
        {
            // notify it that it's being accessed.
            RefreshableCachedObject rco = (RefreshableCachedObject) obj;
            rco.touch();
        }

        return obj;
    }

    /**
     * Adds an object to the cache.
     *
     * @param id The key to store the object by.
     * @param o The object to cache.
     */
    public void addObject(String id,
                          CachedObject o)
    {
        // If the cache already contains the key, remove it and add
        // the fresh one.
        if (cache.containsKey(id))
        {
            cache.remove(id);
        }
        cache.put(id, o);
    }

    /**
     * Removes an object from the cache.
     *
     * @param id The String id for the object.
     */
    public void removeObject(String id)
    {
        cache.remove(id);
    }

    /**
     * Circle through the cache and remove stale objects.  Frequency
     * is determined by the cacheCheckFrequency property.
     */
    public void run()
    {
        while (true)
        {
            // Sleep for amount of time set in cacheCheckFrequency -
            // default = 5 seconds.
            try
            {
                Thread.sleep(cacheCheckFrequency);
            }
            catch (InterruptedException exc)
            {
            }

            clearCache();
        }
    }

    /**
     * Iterate through the cache and remove or refresh stale objects.
     */
    public void clearCache()
    {
        Vector refreshThese = new Vector(20);
        // Sync on this object so that other threads do not
        // change the Hashtable while enumerating over it.
        synchronized (this)
        {
            for (Enumeration e = cache.keys(); e.hasMoreElements();)
            {
                String key = (String) e.nextElement();
                CachedObject co = (CachedObject) cache.get(key);
                if (co instanceof RefreshableCachedObject)
                {
                    RefreshableCachedObject rco = (RefreshableCachedObject) co;
                    if (rco.isUntouched())
                        cache.remove(key);
                    else if (rco.isStale())
                    // Do refreshing outside of sync block so as not
                    // to prolong holding the lock on this object
                        refreshThese.addElement(key);
                }
                else if (co.isStale())
                {
                    cache.remove(key);
                }
            }
        }

        for (Enumeration e = refreshThese.elements(); e.hasMoreElements();)
        {
            String key = (String) e.nextElement();
            CachedObject co = (CachedObject) cache.get(key);
            RefreshableCachedObject rco = (RefreshableCachedObject) co;
            rco.refresh();
        }
    }

    /**
     * Returns the number of objects currently stored in the cache
     *
     * @return int number of object in the cache
     */
    public int getNumberOfObjects()
    {
        return cache.size();
    }

    /**
     * Returns the current size of the cache.
     *
     * @return int representing current cache size in number of bytes
     */
    public int getCacheSize()
            throws IOException
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(baos);
        out.writeObject(cache);
        out.flush();
        //
        // Subtract 4 bytes from the length, because the serialization
        // magic number (2 bytes) and version number (2 bytes) are
        // both written to the stream before the object
        //
        int objectsize = baos.toByteArray().length - 4;
        return objectsize;
    }

    /**
     * Flush the cache of all objects.
     */
    public void flushCache()
    {

        synchronized (this)
        {
            for (Enumeration e = cache.keys(); e.hasMoreElements();)
            {
                String key = (String) e.nextElement();
                cache.remove(key);
            }
        }
    }
}
