org.openlaszlo.cache.Cache.java Source code

Java tutorial

Introduction

Here is the source code for org.openlaszlo.cache.Cache.java

Source

/******************************************************************************
 * Cache.java
 * ****************************************************************************/

/* J_LZ_COPYRIGHT_BEGIN *******************************************************
* Copyright 2001-2007, 2010-2011 Laszlo Systems, Inc.  All Rights Reserved.   *
* Use is subject to license terms.                                            *
* J_LZ_COPYRIGHT_END *********************************************************/

package org.openlaszlo.cache;

import java.io.*;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.collections.SequencedHashMap;
import org.apache.log4j.Logger;
import org.openlaszlo.data.Data;
import org.openlaszlo.utils.ChainedException;
import org.openlaszlo.utils.FileUtils;
import org.openlaszlo.utils.LZHttpUtils;
import org.openlaszlo.xml.internal.XMLUtils;

/**
 * A base class for maintaining a disk-backed cache of requests.
 *
 * Cache has 2 sequenced hash map of Items.  Each item uniquely
 * represents something to be cached (and transcoded).  The
 * first map contains items that are "in memory".  Items that
 * are "in memory" are also stored on disk.  The second map
 * contains items that are only on disk.
 *
 * The cache chucks out (via LRU) items from each map when it is beyond 
 * its maximum size for that map.  Each time an item is requested,
 * it is moved to the front of the "in-memory" map.
 *
 * Each item has its own lock.  When an item is requested, 
 * we lock it and then go to the DataSource to see if 
 * we need to refresh any copy of it we have.
 * When an item has it's size change or it marked dirty, it
 * is also marked as needing its size changed reckoned with the
 * cache sizes.  Whenever the cache is locked and runs into
 * an item that needs reckoning, it updates the cache sizes
 * to reflect the item.
 *
 * The cache supports multiple encodings of individual items.
 *
 * @author <a href="mailto:bloch@laszlosystems.com">Eric Bloch</a>
 */
public abstract class Cache {

    public final String DEFAULT_DISK_MAX = "500000000";
    public final String DEFAULT_MEM_MAX = "1000000";
    public final String DEFAULT_MEM_ITEM_MAX = "0";

    /** Logger. */
    private static Logger mLogger = Logger.getLogger(Cache.class);
    private static Logger mLockLogger = Logger.getLogger("trace.cache.lock");

    /** Name */
    private final String mName;

    /** See the constructor. */
    private File mCacheDirectory;

    /** Sequenced Maps of Cached Items keyed by request.  
     * All access to these maps must must be synchronized
     */
    private SequencedHashMap mDiskMap = null;
    private SequencedHashMap mMemMap = null;
    private SequencedHashMap mActiveMap = null;

    /** Maxmimum size for the on-disk cache in bytes */
    private long mMaxDiskSize = 0;

    /** Maxmimum size for the in-memory cache in bytes */
    private long mMaxMemSize = 0;

    /** Maxmimum size for the in-memory cache item in bytes */
    private long mMaxMemItemSize = 0;

    /** Current size of the in-disk cache in bytes */
    private long mDiskSize = 0;

    /** Current size of the in-memory cache in bytes */
    private long mMemSize = 0;

    /** Current cache id */
    private long mCurName = 0;
    private Object mNameLock = new Object();

    /**
     * Creates a new <code>Cache</code> instance.
     *
     * @param name
     * @param cacheDirectory a <code>File</code> naming a directory
     * where cache files should be kept
     * @param props
     */
    public Cache(String name, File cacheDirectory, Properties props) throws IOException {

        mCacheDirectory = cacheDirectory;
        mName = name;
        cacheDirectory.mkdirs();

        String maxSize;
        String load;
        String mapsize;

        // TODO: 2004-09-07 bloch catch NumberParseException's here
        maxSize = props.getProperty(mName + ".disk.size", DEFAULT_DISK_MAX);
        if (maxSize != null) {
            mMaxDiskSize = Long.parseLong(maxSize);
        }
        maxSize = props.getProperty(mName + ".mem.size", DEFAULT_MEM_MAX);
        if (maxSize != null) {
            mMaxMemSize = Long.parseLong(maxSize);
        }
        maxSize = props.getProperty(mName + ".mem.item.max", DEFAULT_MEM_ITEM_MAX);
        if (maxSize != null) {
            mMaxMemItemSize = Long.parseLong(maxSize);
        }

        load = props.getProperty(mName + ".disk.load");
        if (load != null) {
            int l = Integer.parseInt(load);
            mapsize = props.getProperty(mName + ".disk.mapsize");
            if (mapsize != null) {
                float m = Float.parseFloat(mapsize);
                mDiskMap = new SequencedHashMap(l, m);
            } else {
                mDiskMap = new SequencedHashMap(l);
            }
        } else {
            mDiskMap = new SequencedHashMap();
        }
        load = props.getProperty(mName + ".mem.load");
        if (load != null) {
            int l = Integer.parseInt(load);
            mapsize = props.getProperty(mName + ".mem.mapsize");
            if (mapsize != null) {
                float m = Float.parseFloat(mapsize);
                mMemMap = new SequencedHashMap(l, m);
            } else {
                mMemMap = new SequencedHashMap(l);
            }
        } else {
            mMemMap = new SequencedHashMap();
        }
        load = props.getProperty(mName + ".active.load");
        if (load != null) {
            int l = Integer.parseInt(load);
            mapsize = props.getProperty(mName + ".active.mapsize");
            if (mapsize != null) {
                float m = Float.parseFloat(mapsize);
                mActiveMap = new SequencedHashMap(l, m);
            } else {
                mActiveMap = new SequencedHashMap(l);
            }
        } else {
            mActiveMap = new SequencedHashMap();
        }

        //        long t = System.currentTimeMillis();
        loadFromDirectory();
        //        mLogger.debug(
        /* (non-Javadoc)
         * @i18n.test
         * @org-mes="loading " + p[0] + " took " + p[1] + " seconds"
         */
        //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
        //                                Cache.class.getName(),"051018-183", new Object[] {mName, MathUtils.formatDouble((System.currentTimeMillis() - t) / 1000.0, 2)})
        //        );
    }

    /**
     * this routine is a place holder which can be overridden to hook into
     * the cache loading.  It is used by the cm/CompilationManager to get
     * at the contents of the dependencyTracker as it is being loaded from
     * disk and modify various paths that might have changed since
     * prefetching.
     */
    protected void afterCacheRead(Object metaData) {
    } // override me

    /**
     * @return an item if it exists or null otherwise
     */
    protected synchronized Item getItem(Serializable key) {
        //        mLogger.debug("getItem");

        Item item = (Item) mMemMap.get(key);
        if (item != null) {
            return item;
        }
        return (Item) mDiskMap.get(key);
    }

    /**
     * Find and optionally lock item in the cache that matches this key 
     * If the item doesn't exist, create it.  If the item 
     * does exist, move it to the top of the LRU list by removing
     * and re-adding.   
     * @return the locked item
     */
    protected synchronized Item findItem(Serializable key, String enc, boolean doLockAndLeaveActive)
            throws IOException {

        //mLogger.debug("findItem");

        SequencedHashMap curMap = mMemMap;
        SequencedHashMap newMap = mMemMap;

        boolean hasMemCache = (mMaxMemSize != 0);

        if (!hasMemCache) {
            newMap = mDiskMap;
        }

        Item item = (Item) curMap.get(key);
        if (item == null) {
            curMap = mDiskMap;
            item = (Item) curMap.get(key);
            if (item == null && doLockAndLeaveActive) {
                curMap = mActiveMap;
                item = (Item) curMap.get(key);
                // TODO: [2003-08-27 bloch] add assert in java 1.4 item.active()
            }
        }

        // New items default to the mem cache if it exists.
        // Note that some items that are too big may
        // be cached on disk (and not in mem) but may 
        // temporarily be in the "mem cache" map.
        // This is confusing, but simple and safe to implement
        // w/out complicated locking.  This is because
        // we don't want to lock the maps while we're
        // fetching the items and we won't know the size
        // until we fetch the item.
        //
        // TODO: [2003-09-04 bloch] 
        // Note: this now only happens when doLockAndLeaveActive is
        // false.  At some point we could rearchitect to remove
        // this wart

        try {
            if (item == null) {
                item = new Item(key, enc, hasMemCache);
                if (doLockAndLeaveActive) {
                    item.lock();
                    item.setActive(true);
                }

                //                mLogger.debug(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="Made new item for " + p[0]
                 */
                //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                //                                Cache.class.getName(),"051018-270", new Object[] {key.toString()})
                //);

            } else {
                if (doLockAndLeaveActive) {
                    item.lock();
                    item.setActive(true);
                }
                if (item.needsReckoning()) {
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Reckoning an old item for " + p[0]
                     */
                    //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-285", new Object[] {key.toString()})
                    //);
                    item.reckon();
                }
                if (item.dirty()) {
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Found a dirty item for " + p[0]
                     */
                    //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-296", new Object[] {key.toString()})
                    //);
                    if (doLockAndLeaveActive) {
                        item.removeAndUnlock();
                    } else {
                        item.remove();
                    }
                    curMap.remove(key);
                    item = new Item(key, enc, hasMemCache);
                    if (doLockAndLeaveActive) {
                        item.lock();
                        item.setActive(true);
                    }
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Removed and made new item for " + p[0]
                     */
                    //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-315", new Object[] {key.toString()})
                    //);
                } else {
                    // Remove the item and re-add it below
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Found old item for " + p[0]
                     */

                    //                          org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-325", new Object[] {key.toString()})
                    //);
                    curMap.remove(key);
                    if (curMap == mDiskMap) {
                        if (newMap == mMemMap) {
                            // Update sizes when we're moving from disk to mem
                            long size = item.getSize();
                            if (size <= mMaxMemItemSize || mMaxMemItemSize <= 0) {
                                item.readIntoMemory();
                                // Update sizes after we read into memory in case
                                // the above read fails for some bizarro reason.
                                mDiskSize -= size;
                                mMemSize += size;
                            } else {
                                newMap = mDiskMap;
                            }
                        }
                    }
                }
            }

            if (!doLockAndLeaveActive) {
                newMap.put(key, item);
            } else {
                mActiveMap.put(key, item);
            }

        } catch (IOException e) {
            // If we get any kind of exception, we better unlock the item
            // since no one will be able to unlock it.
            if (doLockAndLeaveActive && item != null) {
                item.unlock();
            }
            throw e;
        } catch (RuntimeException re) {
            // If we get any kind of exception, we better unlock the item
            // since no one will be able to unlock it.
            if (doLockAndLeaveActive && item != null) {
                item.unlock();
            }
            throw re;
        }

        return item;
    }

    /**
     * Reckon this item and update the cache.
     */
    protected synchronized void updateCache(Item item) {

        item.lock();
        item.reckon();
        item.unlock();

        // While we have the cache locked, update the maps
        updateMapsAndReckonItems(false /* leave at least one item in each map */);
    }

    /**
     * Reckon and deactivate this item and update the cache.
     */
    protected synchronized void updateCacheAndDeactivateItem(Item item) {

        item.lock();
        item.reckon();

        if (item.active()) {
            Object key = item.getKey();
            mActiveMap.remove(key);
            if (item.isInMemory()) {
                mMemMap.put(key, item);
            } else {
                mDiskMap.put(key, item);
            }
            item.setActive(false);
        }

        item.unlock();

        // While we have the cache locked, update the maps
        updateMapsAndReckonItems(true);
    }

    /**
     * Set the maximum size of the on-disk cache.
     * Can cause the cache to remove things.
     */
    public synchronized void setMaxDiskSize(long size) {
        if (size >= mMaxDiskSize || size == -1) {
            mMaxDiskSize = size;
            return;
        }
        mMaxDiskSize = size;
        updateMapsAndReckonItems(true);
    }

    /**
     * Get the maximum size of the on-disk cache cache
     */
    public synchronized long getMaxDiskSize() {
        return mMaxDiskSize;
    }

    /**
     * Get the current size of the on-disk cache
     */
    public synchronized long getDiskSize() {
        return mDiskSize;
    }

    /**
     * Set the maxmimum size of the in-memory cache.
     * Can cause the cache to remove things.
     */
    public synchronized void setMaxMemSize(long size) {
        if (size >= mMaxMemSize || size == -1) {
            mMaxMemSize = size;
            return;
        }
        mMaxMemSize = size;
        updateMapsAndReckonItems(true);
    }

    /**
     * Get the maxmimum size of the in-memory cache
     */
    public synchronized long getMaxMemSize() {
        return mMaxMemSize;
    }

    /**
     * Get the current size of the in-memory cache
     */
    public synchronized long getMemSize() {
        return mMemSize;
    }

    /**
     * Update the maps and remove items that
     * push us beyond the maximum size of the
     * cache.  Never empty out the maps.
     * Always leave at least one item if clearAll is false.
     *
     * Also ignore items that are in progress.
     * @param clearAll if false, must leave one elt in each map
     */
    public synchronized void updateMapsAndReckonItems(boolean clearAll) {

        long removedBytes = 0;

        //mLogger.debug(
        /* (non-Javadoc)
         * @i18n.test
         * @org-mes=p[0] + " cache at " + p[1] + " bytes on-disk and " + p[2] + " bytes in-memory."
         */
        //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
        //                                Cache.class.getName(),"051018-482", new Object[] {mName, new Long(mDiskSize), new Long(mMemSize)})
        //        );
        // Infinite mem size... (nothing will get reckoned, hope that's ok)
        if (mMaxMemSize == -1) {
            return;
        }

        int done = 0;
        if (!clearAll) {
            done = 1;
        }

        // If we have a mem cache, push items from memory to disk
        while (mMemSize > mMaxMemSize && mMemMap.size() > done) {
            Map.Entry<?, ?> first = mMemMap.getFirst();
            Object key = first.getKey();
            Item item = (Item) first.getValue();
            item.lock();

            if (item.needsReckoning()) {
                item.reckon();
            }

            if (!item.dirty()) {
                // Some items that were too large for memory
                // might be in this map, but are actually
                // accounted for in the disksize already.
                // We can skip these.
                if (item.mInfo.isInMemory()) {
                    removedBytes = item.removeFromMemory();
                    mMemSize -= removedBytes;
                    mDiskSize += removedBytes;
                } else {
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="mem item was already on disk"
                     */
                    //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-521")
                    //                    );
                }
                //                mLogger.debug(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="Pushing item " + p[0] + " from mem to disk (" + p[1] + " bytes)"
                 */
                //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                //                                Cache.class.getName(),"051018-530", new Object[] {key, new Long(removedBytes)})
                //                );
                mMemMap.remove(key);
                mDiskMap.put(key, item);
            } else {
                if (item.mInfo.isInMemory()) {
                    mMemMap.remove(key);
                    //                    mLogger.debug(
                    /* (non-Javadoc)
                     * @i18n.test
                     * @org-mes="Removing dirty item " + p[0] + " from mem "
                     */
                    //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                    //                                Cache.class.getName(),"051018-543", new Object[] {key})
                    //);
                }
            }
            item.unlock();
        }

        // Infinite disk size... (no disk items will get reckoned, hope that's ok)
        if (mMaxDiskSize == -1) {
            return;
        }

        while (mDiskSize > mMaxDiskSize && mDiskMap.size() > done) {
            Map.Entry<?, ?> first = mDiskMap.getFirst();
            Object key = first.getKey();
            Item item = (Item) first.getValue();
            item.lock();

            if (item.needsReckoning()) {
                item.reckon();
            }

            if (!item.dirty()) {
                removedBytes = item.getSize();
                item.removeAndUnlock();
                //                mLogger.debug(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="Removing item " + p[0] + " from disk (" + p[1] + " bytes)"
                 */
                //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                //                                Cache.class.getName(),"051018-574", new Object[] {key, new Long(removedBytes)})
                // );
                mDiskSize -= removedBytes;
                mDiskMap.remove(key);
            } else {
                mDiskMap.remove(key);
                //                mLogger.debug(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="Removing dirty item " + p[0] + " from disk "
                 */
                //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                //                                Cache.class.getName(),"051018-586", new Object[] {key})
                //);
            }
        }
        //        mLogger.debug(
        /* (non-Javadoc)
         * @i18n.test
         * @org-mes=p[0] + " now at disk: " + p[1] + " bytes."
         */
        //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
        //                                Cache.class.getName(),"051018-596", new Object[] {mName, new Long(mDiskSize)})
        //);
        //        mLogger.debug(
        /* (non-Javadoc)
         * @i18n.test
         * @org-mes=p[0] + " now at mem: " + p[1] + " bytes."
         */
        //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
        //                                Cache.class.getName(),"051018-604", new Object[] {mName, new Long(mMemSize)})
        //);
    }

    /**
     * Dump textual XML representation of cache to buf
     *
     * @param buf buffer to append
     * @param name name for XML element
     * @param details if true, dump details
     */
    public synchronized void dumpXML(StringBuffer buf, String name, boolean details) {

        buf.append("<" + name + " \n");
        buf.append("disk-total=\"");
        buf.append(getMaxDiskSize());
        buf.append("\" ");
        buf.append("disk-in-use=\"");
        buf.append(getDiskSize());
        buf.append("\" ");
        buf.append("mem-total=\"");
        buf.append(getMaxMemSize());
        buf.append("\" ");
        buf.append("mem-in-use=\"");
        buf.append(getMemSize());

        buf.append("\" ");
        buf.append("diskmap-entries=\"");
        buf.append(mDiskMap.size());

        buf.append("\" ");
        buf.append("memmap-entries=\"");
        buf.append(mMemMap.size());

        buf.append("\" ");
        buf.append("activemap-entries=\"");
        buf.append(mActiveMap.size());

        buf.append("\" ");
        buf.append("    >\n");
        if (details) {
            buf.append("<active>\n");
            dumpMap(buf, mActiveMap, false);
            buf.append("</active>\n");
            buf.append("<in-memory>\n");
            dumpMap(buf, mMemMap, true);
            buf.append("</in-memory>\n");
            buf.append("<disk>\n");
            dumpMap(buf, mDiskMap, true);
            buf.append("</disk>\n");
        }
        buf.append("</" + name + ">\n");
    }

    /**
     * @param buf buffer to append
     * @param map map to dump
     */
    private synchronized void dumpMap(StringBuffer buf, SequencedHashMap map, boolean lockItems) {

        Iterator<?> iter = map.iterator();
        while (iter.hasNext()) {
            buf.append("<item ");
            Object key = iter.next();
            Item item = (Item) map.get(key);
            if (lockItems) {
                item.lock();
            }
            String keystring = key.toString();
            if (keystring.length() > 128) {
                keystring = keystring.substring(0, 127) + "...";
            }
            buf.append("key=\"" + XMLUtils.escapeXml(keystring) + "\" ");
            buf.append("in-memory=\"" + Boolean.toString(item.isInMemory()) + "\" ");
            buf.append("dirty=\"" + Boolean.toString(item.dirty()) + "\" ");
            buf.append("active=\"" + Boolean.toString(item.active()) + "\" ");
            buf.append("needs-reckon=\"" + Boolean.toString(item.needsReckoning()) + "\" ");
            buf.append("mem-to-reckon=\"" + item.memToReckon() + "\" ");
            buf.append("disk-to-reckon=\"" + item.diskToReckon() + "\" ");
            buf.append("size=\"" + item.getSize() + "\" ");
            buf.append("key-size=\"" + item.getKeySize() + "\" ");
            buf.append("path-name=\"" + item.getPathName() + "\" ");
            buf.append("info-name=\"" + item.getInfoName() + "\" ");
            long lm = item.getInfo().getLastModified();
            buf.append("last-modified=\"" + lm + "\" ");
            buf.append("last-modified-gmt=\"" + LZHttpUtils.getDateString(lm) + "\" ");
            buf.append("/>\n");
            if (lockItems) {
                item.unlock();
            }
        }
    }

    /**
     * Load the state of the cache from what is in the cache
     * Then update the cache log itself to represent
     * what we now know.
     *
     * FIXME: [2003-03-11] bloch the last-used time is lost when
     * we shutdown; this means the LRU ordering isn't preserved on
     * restart.  We should add lastUsed times to the cachedInfo.
     */
    private synchronized void loadFromDirectory() throws IOException {

        String[] fileNames = mCacheDirectory.list();

        for (int i = 0; i < fileNames.length; i++) {
            if (!fileNames[i].endsWith(".dat"))
                continue;

            int x = fileNames[i].indexOf(".dat");
            String name = fileNames[i].substring(0, x);
            long n;
            try {
                n = Long.parseLong(name);
                if (n > mCurName) {
                    mCurName = n;
                }
            } catch (NumberFormatException ne) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="ignoring a strange .dat file in the cache named: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-717",
                                new Object[] { fileNames[i] }));
                continue;
            }

            Item item = null;
            File file = new File(mCacheDirectory + File.separator + fileNames[i]);
            try {
                item = new Item(file);
            } catch (Throwable e) {
                mLogger.error(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="can't load cache file: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-733",
                                new Object[] { fileNames[i] }));

                mLogger.error(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="exception: "
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-742"), e);
                mLogger.error(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="deleting cache files for : " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-750",
                                new Object[] { fileNames[i] }));
                // Deletes name* files.
                if (!FileUtils.deleteFilesStartingWith(mCacheDirectory, name)) {
                    mLogger.error(
                            /* (non-Javadoc)
                             * @i18n.test
                             * @org-mes="couldn't delete some files with name: " + p[0]
                             */
                            org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-760",
                                    new Object[] { fileNames[i] }));
                }
                continue;
            }

            Object key = item.getKey();
            if (item.isInMemory()) {
                mMemMap.put(key, item);
                mMemSize += item.getSize();
            } else {
                mDiskMap.put(key, item);
                mDiskSize += item.getSize();
            }

            String keystring = key.toString();
            if (keystring.length() > 128) {
                keystring = keystring.substring(0, 127) + "...";
            }
            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="loaded cached item for " + p[0]
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-785", new Object[] {keystring})
            //            );

        }

        updateMapsAndReckonItems(true);

    }

    /**
     * Clear the cache
     */
    public synchronized boolean clearCache() {
        mLogger.info(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="Clearing " + p[0]
                 */
                org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-804",
                        new Object[] { mName }));

        boolean ok1 = clearMap(mActiveMap);
        boolean ok2 = clearMap(mDiskMap);
        boolean ok3 = clearMap(mMemMap);
        boolean ok4 = true;

        mMemSize = 0;
        mDiskSize = 0;

        ok4 = FileUtils.deleteFiles(mCacheDirectory);

        return ok1 && ok2 && ok3 && ok4;
    }

    /**
     * Clear entries from given map
     * @param map to clear
     */
    private boolean clearMap(SequencedHashMap map) {

        boolean ok = true;
        while (map.size() > 0) {
            Map.Entry<?, ?> first = map.getFirst();
            Object key = first.getKey();
            Item item = (Item) first.getValue();
            item.lock();
            if (!item.removeAndUnlock()) {
                ok = false;
            }
            map.remove(key);
        }

        return ok;
    }

    /**
     * Return a unique name
     *
     * Since time always moves fwd, we shouldn't
     * have to worry about using names we've
     * previously "allocated".
     *
     * FIXME: [2003-02-25 bloch] if someone sets the clock
     * back, we need to clear the cache; we could easily
     * detect this using timestamps on files in the cache.
     */
    private long nextUniqueName() {

        long name;

        synchronized (mNameLock) {
            name = System.currentTimeMillis();
            if (name <= mCurName) {
                name = mCurName + 1;
            }
            mCurName = name;
        }

        return name;
    }

    /**
     * A class that represents an item in the Cache.
     *
     * @author <a href="mailto:bloch@laszlosystems.com">Eric Bloch</a>
     */
    @SuppressWarnings("serial")
    protected class Item implements Serializable {

        transient private Lock mLock;
        transient private boolean mDirty = false;
        transient private boolean mNeedsReckoning = false;
        /** true if the item is actively being updated */
        transient private boolean mActive = false;

        transient private byte mBuffer[];

        transient private long mMemSizeToReckon = 0;
        transient private long mDiskSizeToReckon = 0;

        /**
         * Information about this cached item.
         */
        CachedInfo mInfo = null;

        /** 
         * Construct an Item based on this request
         * and lock it for write.
         */
        public Item(Serializable key, String enc, boolean inMemory) {

            mLock = new ReentrantLock();
            mInfo = new CachedInfo(key, enc, inMemory, nextUniqueName());
            mBuffer = null;
        }

        /**
         * @return true if item is dirty
         */
        public boolean dirty() {
            return mDirty;
        }

        /**
         * @return true if item is active
         */
        public boolean active() {
            return mActive;
        }

        /** set if item is active */
        public void setActive(boolean a) {
            mActive = a;
        }

        /**
         * @return true if item is dirty
         */
        public boolean needsReckoning() {
            return mNeedsReckoning;
        }

        /**
         * @return mem to reckon
         */
        public long memToReckon() {
            return mMemSizeToReckon;
        }

        /**
         * @return disk to reckon
         */
        public long diskToReckon() {
            return mDiskSizeToReckon;
        }

        /**
         * Update cache sizes with size from item
         * that haven't been reckonned yet.
         * Must have item and entire cache locked to call this
         */
        public void reckon() {
            mMemSize += mMemSizeToReckon;
            mDiskSize += mDiskSizeToReckon;

            mDiskSizeToReckon = 0;
            mMemSizeToReckon = 0;

            mNeedsReckoning = false;
            //            mLogger.debug("reckoned an item");
        }

        /**
         * Item should be locked during this call
         */
        public void readIntoMemory() throws IOException {
            FileInputStream fis = null;
            try {
                String p = getPathName();
                fis = new FileInputStream(p);
                mBuffer = new byte[(int) mInfo.getSize()];
                fis.read(mBuffer, 0, (int) mInfo.getSize());
                mInfo.setInMemory(true);
            } catch (IOException e) {
                markDirty();
                throw e;
            } finally {
                FileUtils.close(fis);
            }
        }

        /**
         * Item should be locked during this call
         * @return size of item
         * caller is responsible for updating cache sizes
         */
        public long removeFromMemory() {
            mBuffer = null;
            mInfo.setInMemory(false);
            return mInfo.getSize();
        }

        /** 
         * Construct an Item based on the contents of this file
         */
        public Item(File f)
                throws IOException, OptionalDataException, StreamCorruptedException, ClassNotFoundException {
            mLock = new ReentrantLock();

            InputStream in = null;
            try {
                in = new BufferedInputStream(new FileInputStream(f));
                ObjectInputStream istr = new ObjectInputStream(in);
                mInfo = (CachedInfo) istr.readObject();
                // after reading the object, call our override routine
                afterCacheRead(mInfo.getMetaData());
            } finally {
                FileUtils.close(in);
            }

            // FIXME: [2003-08-15 pkang] isInMemory() will always evaluate to
            // true for items that are read from disk. This is because those
            // items are written out only when they're fetched and, by default,
            // they are created to exist in memory. Since it's better that we
            // don't read every item into memory during start-up, isInMemory is
            // set to false here.

            mInfo.setInMemory(false);

            //            if (mInfo.isInMemory()) {
            //                readIntoMemory();
            //            }
        }

        /**
         * Lock the item
         */
        public void lock() {
            if (mLockLogger.isTraceEnabled()) {
                mLockLogger.trace(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="acquiring lock: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1032",
                                new Object[] { getKey() }));
            }
            try {
                mLock.lockInterruptibly();
            } catch (InterruptedException e) {
                throw new ChainedException(e);
            }
            if (mLockLogger.isTraceEnabled()) {
                mLockLogger.trace(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="acquired lock: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1045",
                                new Object[] { getKey() }));
            }
        }

        /*
         * Done with item
         */
        public void unlock() {
            if (mLockLogger.isTraceEnabled()) {
                mLockLogger.trace(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="releasing read lock: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1059",
                                new Object[] { getKey() }));
            }
            mLock.unlock();
            if (mLockLogger.isTraceEnabled()) {
                mLockLogger.trace(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="released read lock: " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1068",
                                new Object[] { getKey() }));
            }
        }

        /**
         * @return the key for this item.
         */
        public Serializable getKey() {
            if (mInfo != null)
                return mInfo.getKey();
            else
                return "uninitialized item";
        }

        /**
         * @return whether this item is in memory or not
         */
        public boolean isInMemory() {
            if (mInfo != null)
                return mInfo.isInMemory();
            else
                return false;
        }

        /**
         * Return the cached size of this item in bytes 
         * Must have the item read locked for this.
         */
        public long getSize() {
            return mInfo.getSize();
        }

        /**
         * Return the size of this items key in bytes.
         * May return -1 if the size is unknown/expensive to compute.
         * Must have the item read locked for this.
         */
        public long getKeySize() {
            return mInfo.getKeySize();
        }

        /**
         * @return item's info
         */
        public CachedInfo getInfo() {
            return mInfo;
        }

        /**
         * Return the path name of the cached version of this item
         */
        public String getPathName() {
            // FIXME: [2003-03-03 bloch] unhardcode gz extension someday
            return mCacheDirectory.getAbsolutePath() + File.separator + mInfo.getName() + ".swf"
                    + (mInfo.getEncoding() == null ? "" : ".gz");
        }

        /**
         * Return the path name of the cached info for this item
         */
        public String getInfoName() {
            return mCacheDirectory.getAbsolutePath() + File.separator + mInfo.getName() + ".dat";
        }

        /**
         * Encoding the given item using the requested encoding.
         * Encode to a temporary file and then rename.
         *
         * @return the size in bytes of the encoded file
         * caller is responsible for updating item and cache sizes
         */
        public long encode(String enc) throws IOException {

            File f = new File(getPathName());
            File tempDir = f.getParentFile();
            File tempFile = File.createTempFile("lzuc-", null, tempDir);
            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="Temporary file is " + p[0]
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-1152", new Object[] {tempFile.getAbsolutePath()})
            //);
            FileUtils.encode(f, tempFile, enc);
            long size = tempFile.length();
            if (!f.delete()) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't delete " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1163",
                                new Object[] { f.getAbsolutePath() }));
            }
            if (!tempFile.renameTo(f)) {
                mLogger.error(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't rename temporary file to " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1173",
                                new Object[] { f.getAbsolutePath() }));
            }
            return size;
        }

        /**
         * Mark the item as dirty
         */
        public void markDirty() {
            if (mDirty) {
                return;
            }
            // Removed the cache file and any buffer and mark the item
            // as dirty
            File f = new File(getPathName());
            if (f.exists() && !f.delete()) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't delete " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1163",
                                new Object[] { getPathName() }));
            }
            f = new File(getInfoName());
            if (f.exists() && !f.delete()) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't delete " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1163",
                                new Object[] { getInfoName() }));
            }
            mDirty = true;
            mBuffer = null;
            mNeedsReckoning = true;
            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="Marking item dirty: " + p[0]
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-1219", new Object[] {mInfo.getKey()})
            //);
            if (mInfo.isInMemory()) {
                mMemSizeToReckon -= mInfo.getSize();
            } else {
                mDiskSizeToReckon -= mInfo.getSize();
            }
            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="disk to reckon : " + p[0] + ", mem to reckon : " + p[1]
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-1232", new Object[] {new Long(mDiskSizeToReckon), new Long(mMemSizeToReckon)})
            //            );
            mInfo.setSize(0);
        }

        /**
         * @return true if this item is still valid
         * given the current data
         */
        public boolean validForData(Data d) {
            // TODO: [2003-01-14 bloch] Make sure the cached 
            // ETag, Content-Length, and Content-MD5 are ok.
            // Need to add these to the CachedInfo class and put
            // logic here to deal with missing/present values.
            // Will need to get the right interface between
            // Item/CachedInfo/Data to make this work.
            //
            //if (hdr != null) {
            //}

            return true;
        }

        /**
         * Remove the cached version of this item and then unlock it.
         * Must have item locked for this.  Caller is responsible for updatng cache sizes
         * @return true if there were no problems
         */
        public boolean removeAndUnlock() {
            boolean ok = remove();
            unlock();
            return ok;
        }

        /**
         * Remove the cached version of this item 
         * @return true if there were no problems
         * Caller is responsible for updatng cache sizes.
         */
        private boolean remove() {

            boolean ok = true;
            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="removing item for " + p[0] + " (" + p[1] + " - " + p[2] + " bytes) ."
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-1280", new Object[] {mInfo.getKey(), getPathName(), new Long(getSize())})
            //            );
            File f = new File(getPathName());
            if (f.exists() && !f.delete()) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't delete " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1163",
                                new Object[] { getPathName() }));
                ok = false;
            }
            f = new File(getInfoName());
            if (f.exists() && !f.delete()) {
                mLogger.warn(
                        /* (non-Javadoc)
                         * @i18n.test
                         * @org-mes="Can't delete " + p[0]
                         */
                        org.openlaszlo.i18n.LaszloMessages.getMessage(Cache.class.getName(), "051018-1163",
                                new Object[] { getInfoName() }));
                ok = false;
            }

            return ok;
        }

        /**
         * @return stream depending on whether we're in mem
         * or on disk
         */
        public InputStream getStream() {
            if (mInfo.isInMemory()) {
                return new ByteArrayInputStream(mBuffer);
            } else {
                try {
                    return new FileInputStream(getPathName());
                } catch (FileNotFoundException e) {
                    throw new ChainedException(e.getMessage());
                }
            }
        }

        /**
         * @return returns raw byte-array data.
         * This should only be used to retrieve immutable data! If the caller modifies this byte array,
         * it could cause trouble.
         */
        public byte[] getRawByteArray() {
            try {
                if (!mInfo.isInMemory()) {
                    readIntoMemory();
                }
                if (mInfo.isInMemory()) {
                    return mBuffer;
                } else {
                    ByteArrayOutputStream bos = null;
                    FileInputStream instream = null;
                    try {
                        // If it's still not in memory, maybe it was too big, so let's copy it into
                        // a new byte array and return that.
                        bos = new ByteArrayOutputStream();
                        instream = new FileInputStream(getPathName());
                        FileUtils.send(instream, bos);
                        return bos.toByteArray();
                    } finally {
                        if (bos != null) {
                            bos.close();
                        }
                        if (instream != null) {
                            instream.close();
                        }
                    }
                }
            } catch (IOException e) {
                throw new ChainedException(e.getMessage());
            }
        }

        /**
         * @return the file object 
         */
        public File getFile() {
            return new File(getPathName());
        }

        /**
         * Get the meta data for the current item
         */
        public Serializable getMetaData() {
            return mInfo.getMetaData();
        }

        /**
         * Update the item from the given input stream
         * and meta data and mark the item as clean 
         * (not dirty).
         *
         * @param in input stream; ignored if null
         * @param metaData
         */
        public void update(InputStream in, Serializable metaData) throws IOException {

            //            mLogger.debug(
            /* (non-Javadoc)
             * @i18n.test
             * @org-mes="updating item stream and metadata"
             */
            //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
            //                                Cache.class.getName(),"051018-1399")
            //);
            update(metaData);

            if (in != null) {
                update(in);
            }

            markClean();
        }

        /**
         * Update the item from the given input stream
         *
         * @param in input stream
         */
        public void update(InputStream in) throws IOException {

            FileOutputStream out = null;

            try {

                //                mLogger.debug(
                /* (non-Javadoc)
                 * @i18n.test
                 * @org-mes="updating item from stream"
                 */
                //                        org.openlaszlo.i18n.LaszloMessages.getMessage(
                //                                Cache.class.getName(),"051018-1428")
                //);

                out = new FileOutputStream(getPathName());
                long size = FileUtils.send(in, out);
                FileUtils.close(out);

                String enc = mInfo.getEncoding();
                if (enc != null && !enc.equals("")) {
                    size = encode(enc);
                }

                long oldSize = mInfo.getSize();
                mInfo.setSize(size);
                mNeedsReckoning = true;

                // Remove size from any old item
                if (mInfo.isInMemory()) {
                    mMemSizeToReckon -= oldSize;
                } else {
                    mDiskSizeToReckon -= oldSize;
                }

                // Check to see if we're too big for memory
                if (mInfo.isInMemory() && mMaxMemItemSize > 0 && size > mMaxMemItemSize) {
                    mLogger.trace("this item too big for memory ");
                    mInfo.setInMemory(false);
                }

                if (mInfo.isInMemory()) {
                    mLogger.trace("reading item into memory ");
                    readIntoMemory();
                    mMemSizeToReckon += size;
                } else {
                    mDiskSizeToReckon += size;
                }

            } finally {
                FileUtils.close(out);
            }
        }

        /**
         * Mark the item as clean
         */
        public void markClean() {
            mDirty = false;
        }

        /**
         * Update the item's meta data.
         *
         * @param metaData
         */
        public void update(Serializable metaData) throws IOException {

            mInfo.setMetaData(metaData);
            updateInfo();
        }

        /**
         * Update the item's info
         */
        public void updateInfo() throws IOException {

            FileOutputStream out = null;

            try {

                // Write out the info (.dat) file
                out = new FileOutputStream(getInfoName());
                ObjectOutputStream ostr = new ObjectOutputStream(out);
                ostr.writeObject(mInfo);
                ostr.flush();
                ostr.close();

            } finally {
                FileUtils.close(out);
            }
        }
    }
}