/* $Id: CacheManager.java 988245 2010-08-23 18:39:35Z kwright $ */

/**
* 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.
*/
package org.apache.manifoldcf.core.cachemanager;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.manifoldcf.core.interfaces.ICacheClass;
import org.apache.manifoldcf.core.interfaces.ICacheCreateHandle;
import org.apache.manifoldcf.core.interfaces.ICacheDescription;
import org.apache.manifoldcf.core.interfaces.ICacheExecutor;
import org.apache.manifoldcf.core.interfaces.ICacheHandle;
import org.apache.manifoldcf.core.interfaces.ICacheManager;
import org.apache.manifoldcf.core.interfaces.ILockManager;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.LockManagerFactory;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.interfaces.StringSet;
import org.apache.manifoldcf.core.interfaces.StringSetBuffer;
import org.apache.manifoldcf.core.system.Logging;

/** This class implements the cache manager interface, and provides generic cache management
* services.  See the interface for a description of how the services work.  However, since this
* service requires the lock manager, and there is one lock manager per thread, there will
* be one of these service instances per thread as well.
*/
public class CacheManager implements ICacheManager
{
  public static final String _rcsid = "@(#)$Id: CacheManager.java 988245 2010-08-23 18:39:35Z kwright $";

  protected final static String cacheLockPrefix = "_Cache_";

  protected ILockManager lockManager;
  protected static GeneralCache cache = new GeneralCache();

  // This is the hash mapping transaction id's to CacheTransactionHandle objects.
  // It is thread specific because transactions are thread local.
  protected HashMap transactionHash = new HashMap();

  public CacheManager(IThreadContext context)
    throws ManifoldCFException
  {
    lockManager = LockManagerFactory.make(context);
  }

  /** Locate or create a set of objects in the cached object pool, and/or destroy and invalidate
  * the same or other objects.
  *
  * For each "locate" object
  * described below, one of two things will happen: the execObject's exists() method
  * will be called (if the object exists in the cache already), or the execObject's create*()
  * family of methods will be called.  When all objects have been declared to the execObject,
  * finally the execute() method will be called.  In both cases, all objects are considered
  * to be locked, and cannot be invalidated until the lock is cleared (after the end of the called method).
  *
  * For the invalidation descriptions, the execObject's destroy() method will be called instead.
  * The object will be invalidated from the cache at the end of the findObjectsAndExecute() operation.
  *
  * It is perfectly legal to include the same object in both the locate set and the invalidate set!
  *
  * If an error occurs during object creation, the execute() method will NOT be called.  If the
  * execute() method has an error, the objects that were created will still be recorded in the cache.
  *
  * @param locateObjectDescriptions is a set of description objects that uniquely describe the objects needed.
  * @param invalidateKeys is the set of keys to invalidate at the end of the execution.
  * @param execObject is the cache execution object whose create() or execute() methods will
  * be called.
  * @param transactionID is the current transaction identifier, or null.  Objects created within this call
  * will be associated with this transaction; they will be purged from the cache should the transaction
  * be rolled back.
  */
  public void findObjectsAndExecute(ICacheDescription[] locateObjectDescriptions, StringSet invalidateKeys,
    ICacheExecutor execObject, String transactionID)
    throws ManifoldCFException
  {
    // There is some clever engineering involved here.  The lock manager is used to control synchronization
    // around usage and invalidation of objects.  However, there is ANOTHER lock condition that needs to be
    // managed as well in this code.  Specifically, in a single JVM, only one thread should be allowed to
    // create an object at a time.
    //
    // This is not a true "lock", because it doesn't cross JVM boundaries.  However, it is not brute-force
    // like a synchronizer either (a synchronizer would single-thread all object creation and usage).
    // Instead, we need to create a specific object for every object being located.  Then, a synchronizer
    // on that object can insure that only one thread per JVM is allowed to create a given object at a time,
    // and the others see an "existing" object instead.
    //
    // The basic logic is that the sync object is looked up from a global pool in a synchronized way, and if
    // found indicates that a creation is in progress on the corresponding object.  The thread must then wait
    // until the synchronizer object is released before continuing.  Since true locks have already been thrown,
    // it is guaranteed that the object cannot be destroyed, and the released threads can refind the object
    // and use it as "existing".  Basically, it's a runt version of the lock manager.

    ICacheHandle handle = enterCache(locateObjectDescriptions,invalidateKeys,transactionID);
    try
    {
      // Ok, we locked everything we need.
      // Now we need to create what is not yet there.
      // First, call create() or exists() on the "locate" objects (and also update the
      // LRU times)
      if (locateObjectDescriptions != null)
      {
        // We are going to go through the locate object list twice; once to see
        // what we need to create, and a second time after we do the creation.

        // Accumulate non-cached objects here.
        HashMap allObjects = new HashMap();
        int i;

        ICacheCreateHandle createHandle = enterCreateSection(handle);
        try
        {
          // Now, go through the objects again, and see which ones we need
          // to create.  The ones with null cache sets will always need creation.
          ArrayList createList = new ArrayList();
          i = 0;
          while (i < locateObjectDescriptions.length)
          {
            ICacheDescription objectDescription = locateObjectDescriptions[i++];
            StringSet set = objectDescription.getObjectKeys();
            if (set == null)
              createList.add(objectDescription);
            else
            {
              Object o = lookupObject(createHandle,objectDescription);
              if (o == null)
                createList.add(objectDescription);
              else
                allObjects.put(objectDescription,o);
            }
          }
          // Perform the create operation
          ICacheDescription[] createDescriptions = new ICacheDescription[createList.size()];
          i = 0;
          while (i < createList.size())
          {
            createDescriptions[i] = (ICacheDescription)createList.get(i);
            i++;
          }
          Object[] createdObjects = execObject.create(createDescriptions);
          if (createdObjects == null)
            return;
          // Loop through the returned objects, and enter them into the cache
          i = 0;
          while (i < createdObjects.length)
          {
            saveObject(createHandle,createDescriptions[i],createdObjects[i]);
            allObjects.put(createDescriptions[i],createdObjects[i]);
            i++;
          }
        }
        finally
        {
          leaveCreateSection(createHandle);
        }

        i = 0;
        while (i < locateObjectDescriptions.length)
        {
          ICacheDescription objectDescription = locateObjectDescriptions[i++];
          Object o = allObjects.get(objectDescription);
          // It already exists - send it to the executor
          execObject.exists(objectDescription,o);
        }
      }

      // Now, call the execute method
      if (execObject != null)
        execObject.execute();

      invalidateKeys(handle);
    }
    finally
    {
      leaveCache(handle);
    }
  }

  /** Second way of doing cache management.
  * Basically, this approach breaks the findObjectsAndExecute() method down into bite-sized chunks.
  * The goal is additional flexibility, and an executor object does not need to be defined.
  * Pretty much everything else is identical.  The objects returned by the manager methods
  * provide context control and enforce the proper ordering and nesting.
  * This method enters the cacher and builds a cache handle.  Once a cache handle is
  * returned, the calling code MUST, under all circumstances, leave the cache using the
  * same handle.
  * @param locateObjectDescriptions is a set of description objects that uniquely describe the objects needed.
  * May be null if no objects are desired.
  * @param invalidateKeys are the keys to invalidate after successful execution.  May be null.
  * @param transactionID is the current transaction identifier, or null.  Objects created within this block
  * will be associated with this transaction; they will be purged from the cache should the transaction
  * be rolled back.
  * @return a cache handle.
  */
  public ICacheHandle enterCache(ICacheDescription[] locateObjectDescriptions, StringSet invalidateKeys,
    String transactionID)
    throws ManifoldCFException
  {
    if (Logging.cache.isDebugEnabled())
    {
      StringBuilder sb = new StringBuilder();
      if (locateObjectDescriptions != null)
      {
        sb.append("{");
        int i = 0;
        while (i < locateObjectDescriptions.length)
        {
          if (i > 0)
            sb.append(",");
          sb.append(locateObjectDescriptions[i].getCriticalSectionName());
          i++;
        }
        sb.append("}");
      }
      else
        sb.append("NULL");

      StringBuilder inv = new StringBuilder();
      if (invalidateKeys != null)
      {
        inv.append("{");
        boolean isFirst = true;
        Iterator iter = invalidateKeys.getKeys();
        while (iter.hasNext())
        {
          if (isFirst)
            isFirst = false;
          else
            inv.append(",");
          inv.append((String)iter.next());
        }
        inv.append("}");
      }
      else
        inv.append("NULL");

      Logging.cache.debug("Entering cacher; objects = "+sb.toString()+" invalidate keys = "+inv.toString());
    }

    // First, throw read locks for the locate objects and write locks for invalidate objects.
    // To do the read locks, we need to go through the locate objects and get their cache keys.
    // This must be done prior to the object even being created.
    StringSetBuffer readLockTable = new StringSetBuffer();
    if (locateObjectDescriptions != null)
    {
      int i = 0;
      while (i < locateObjectDescriptions.length)
      {
        ICacheDescription objectDescription = locateObjectDescriptions[i++];
        // Get the keys
        StringSet keys = objectDescription.getObjectKeys();
        if (keys != null)
        {
          readLockTable.add(keys);
        }
      }
    }
    // Convert readLockTable to a StringSet
    StringSet readKeys = new StringSet(readLockTable);

    // The locks we will actually throw depend on whether or not we are in a transaction.  The
    // transaction filters the locks that are needed; locks are preserved within the transaction.
    // The cache handle itself also does not need the same data - in a transaction, readLocks and writeLocks
    // passed to the handle will be null (because we don't want them to be freed until the transaction exits).
    CacheHandle ch;
    if (transactionID == null)
    {
      String[] writeLocks = null;
      if (invalidateKeys != null)
        writeLocks = invalidateKeys.getArray(cacheLockPrefix);
      String[] readLocks = readKeys.getArray(cacheLockPrefix);
      ch = new CacheHandle(readLocks,writeLocks,locateObjectDescriptions,invalidateKeys,transactionID);
      Logging.lock.debug("Starting cache outside transaction");
      lockManager.enterLocks(readLocks,null,writeLocks);
      Logging.lock.debug(" Done starting cache");
    }
    else
    {
      CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID);
      if (handle == null)
      {
        ManifoldCFException ex = new ManifoldCFException("Illegal transaction ID: '"+transactionID+"'",ManifoldCFException.GENERAL_ERROR);
        Logging.cache.error(Thread.currentThread().toString()+": enterCache: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex);
        throw ex;
      }

      if (Logging.lock.isDebugEnabled())
        Logging.lock.debug("Starting cache in transaction "+transactionID);

      ch = new CacheHandle(null,null,locateObjectDescriptions,invalidateKeys,transactionID);
      StringSet newReadLocks = handle.getRemainingReadLocks(readKeys,invalidateKeys);
      StringSet newWriteLocks = handle.getRemainingWriteLocks(readKeys,invalidateKeys);
      lockManager.enterLocks(newReadLocks.getArray(cacheLockPrefix),null,newWriteLocks.getArray(cacheLockPrefix));
      handle.addLocks(newReadLocks,newWriteLocks);
      // Logging.lock.debug(" Done starting cache");
    }

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Successfully entered cacher; handle = "+ch.toString());
    }

    return ch;
  }

  /** Enter a creation critical section.  This insures that only one thread is
  * creating the specified objects at a time.  This MUST be paired with
  * a leaveCreateSection() method call, whatever happens.
  *@param handle is the cache handle.
  */
  public ICacheCreateHandle enterCreateSection(ICacheHandle handle)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Entering cache create section; cache handle = "+handle.toString());
    }

    // Since we are inside the locks, any existing object cannot be invalidated;
    // however, it is possible that more than one thread can attempt to perform
    // object creation for the same object.  The only way to prevent this is to
    // throw a multiple critical section writelock on all the objects.  This will
    // stop multiple creation.
    ICacheDescription[] locateObjectDescriptions = handle.getObjectDescriptions();
    if (locateObjectDescriptions == null)
      throw new ManifoldCFException("Can't enter create section without objects to create",
      ManifoldCFException.GENERAL_ERROR);
    int i = 0;
    ArrayList writeCriticalSectionArray = new ArrayList();
    while (i < locateObjectDescriptions.length)
    {
      ICacheDescription objectDescription = locateObjectDescriptions[i++];
      StringSet set = objectDescription.getObjectKeys();

      // If this object is uncached, we do NOT try to synchronize creation, since it is
      // a waste of time to do that.  Multiple objects will need to be created anyhow.
      if (set != null)
        writeCriticalSectionArray.add(objectDescription.getCriticalSectionName());
    }
    // Make it into an array.
    String[] writeCriticalSections = new String[writeCriticalSectionArray.size()];
    i = 0;
    while (i < writeCriticalSectionArray.size())
    {
      writeCriticalSections[i] = (String)writeCriticalSectionArray.get(i);
      i++;
    }

    CacheCreateHandle ch = new CacheCreateHandle(writeCriticalSections,handle.getTransactionID());
    // Enter critical section (but only based on the objects we intend to cache).
    lockManager.enterCriticalSections(null,null,writeCriticalSections);

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Successfully entered cache create section for handle = "+
        handle.toString()+"; section handle = "+ch.toString());
    }

    return ch;
  }

  /** Lookup an object.  Returns null if object not found.  If it is found,
  * object's LRU and expiration info are updated.  The objectDescription passed
  * MUST be one of the ones specified in the enclosing enterCache() method.
  *@param handle is the handle to use for the create.
  *@param objectDescription is the description of the object to look up.
  *@return the object, or null if not found.
  */
  public Object lookupObject(ICacheCreateHandle handle, ICacheDescription objectDescription)
    throws ManifoldCFException
  {
    if (handle == null)
      throw new ManifoldCFException("Can't do lookup outside of create section",
      ManifoldCFException.GENERAL_ERROR);

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Looking up object in section handle = "+handle.toString()+
        "; object name = '"+objectDescription.getCriticalSectionName()+"'");
    }

    StringSet set = objectDescription.getObjectKeys();
    // If no cache keys, it can't be cached, so don't check!
    if (set == null)
      return null;

    // If this is in a transaction, we must look at the local cached copy first.
    // In fact, we walk back through the chain of parent transactions until we find it,
    // or until the cache keys are invalid against the transaction's invalidation keys.
    String transactionID = handle.getTransactionID();
    if (transactionID != null)
    {
      CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID);
      if (transactionHandle == null)
      {
        ManifoldCFException ex = new ManifoldCFException("Illegal transaction id",ManifoldCFException.GENERAL_ERROR);
        Logging.cache.error(Thread.currentThread().toString()+": lookupObject: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex);
        throw ex;
      }
      while (transactionHandle != null)
      {
        // Look for the object
        Object q = transactionHandle.lookupObject(objectDescription);
        if (q != null)
        {
          if (Logging.cache.isDebugEnabled())
          {
            Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' found in transaction cache");
          }
          return q;
        }
        // See if we can look at the parent
        if (transactionHandle.checkCacheKeys(set))
          return null;
        transactionHandle = transactionHandle.getParentTransaction();
      }
      // If nothing stops us, look in the global cache too
    }

    Object o = cache.lookup(objectDescription);
    if (o == null)
      return null;

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' exists locally; checking if local copy is valid");
    }

    // See if the object's attached expiration is before the current time.
    long expireTime = cache.getObjectExpirationTime(objectDescription);
    if (expireTime != -1L && expireTime <= handle.getLookupTime())
    {
      // Blow away the entry in cache, since it has expired
      cache.deleteObject(objectDescription);
      return null;
    }
    
    // Before we conclude that the object is found, if we are on a multi-JVM environment we MUST check
    // the object's timestamp!!!  We check it against the invalidation key file timestamps for the object.
    long createTime = cache.getObjectCreationTime(objectDescription);
    StringSet keys = cache.getObjectInvalidationKeys(objectDescription);

    Iterator iter = keys.getKeys();
    while (iter.hasNext())
    {
      String key = (String)iter.next();
      if (hasExpired(key,createTime))
      {
        // Blow away the entry in cache, since it has expired
        cache.deleteObject(objectDescription);
        return null;
      }
    }

    // System.out.println("Found object: "+objectDescription.getCriticalSectionName());

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(" Object '"+objectDescription.getCriticalSectionName()+"' is valid; resetting local expiration");
    }

    // Update the expiration time for this object.
    resetObjectExpiration(objectDescription,handle.getLookupTime());

    return o;
  }

  /** Check if object has expired (by looking at file system).
  *@param key is the invalidation key.
  *@param createTime is the creation time.
  *@return true if expired, false otherwise.
  */
  protected boolean hasExpired(String key, long createTime)
    throws ManifoldCFException
  {
    long createdDate = readSharedData(key);
    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(" Checking whether our cached copy of object with key = "+key+" has been invalidated.  It has create time "+Long.valueOf(createTime).toString()+", and the last change is "+Long.valueOf(createdDate).toString());
    }
    if (createdDate == 0L)
      return false;
    return createdDate >= createTime;
  }

  /** Set object's expiration and LRU.
  *@param objectDescription is the description object.
  *@param currentTime is the current time in milliseconds since epoch.
  */
  protected void resetObjectExpiration(ICacheDescription objectDescription, long currentTime)
  {

    // Update the expiration time for this object.
    long expireInterval = objectDescription.getObjectExpirationTime(currentTime);
    cache.setObjectExpiration(objectDescription,expireInterval);

    // Update LRU and max counts.  This also flushes the cache to meet the criteria.
    ICacheClass objectClass = objectDescription.getObjectClass();
    if (objectClass != null)
      cache.setObjectClass(objectDescription,objectClass.getClassName(),objectClass.getMaxLRUCount());
    else
      cache.setObjectClass(objectDescription,null,Integer.MAX_VALUE);

  }

  /** Save a newly created object.  The object MUST be one of those identified in the
  * enterCache() method.
  *@param handle is the create handle.
  *@param objectDescription is the object description.
  *@param object is the object.
  */
  public void saveObject(ICacheCreateHandle handle, ICacheDescription objectDescription,
    Object object)
    throws ManifoldCFException
  {
    if (handle == null)
      throw new ManifoldCFException("Can't do save outside of create section",
      ManifoldCFException.GENERAL_ERROR);

    if (Logging.cache.isDebugEnabled())
    {
      StringSet ks = objectDescription.getObjectKeys();
      StringBuilder sb = new StringBuilder();
      if (ks != null)
      {
        sb.append("{");
        Iterator iter = ks.getKeys();
        boolean isFirst = true;
        while (iter.hasNext())
        {
          if (isFirst)
            isFirst = false;
          else
            sb.append(",");
          sb.append((String)iter.next());
        }
        sb.append("}");
      }
      else
        sb.append("NULL");

      Logging.cache.debug("Saving new object in section handle = "+handle.toString()+
        "; object description = '"+objectDescription.getCriticalSectionName()+
        "; cache keys = "+sb.toString());
    }


    StringSet keys = objectDescription.getObjectKeys();
    if (keys != null)
    {
      String transactionID = handle.getTransactionID();
      if (transactionID == null)
      {
        cache.setObject(objectDescription,object,keys,handle.getLookupTime());
        // Update the expiration time for this object.
        resetObjectExpiration(objectDescription,handle.getLookupTime());
      }
      else
      {
        // Put it into the transaction object
        // Expiration and LRU don't count here; they get applied when the object goes into
        // the global cache.
        CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID);
        if (transactionHandle == null)
        {
          ManifoldCFException ex = new ManifoldCFException("Bad transaction handle",ManifoldCFException.GENERAL_ERROR);
          Logging.cache.error(Thread.currentThread().toString()+": saveObject: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex);
          throw ex;
        }
        transactionHandle.saveObject(objectDescription,object,keys);
      }
    }
  }

  /** Leave the create section.
  *@param handle is the handle created by the corresponding enterCreateSection() method.
  */
  public void leaveCreateSection(ICacheCreateHandle handle)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Leaving cache create section; section handle = "+handle.toString());
    }

    lockManager.leaveCriticalSections(null,null,handle.getCriticalSectionNames());
  }

  /** Invalidate keys.  The keys invalidated are what got passed to the enterCache() method.
  *@param handle is the cache handle.  Does nothing if a null set of keys was passed in.
  */
  public void invalidateKeys(ICacheHandle handle)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Invalidating keys; handle = "+handle.toString());
    }


    StringSet invalidateKeys = handle.getInvalidationKeys();
    // Get the transaction handle in effect
    String transactionID = handle.getTransactionID();
    if (transactionID == null)
    {
      performInvalidation(invalidateKeys);
    }
    else
    {
      CacheTransactionHandle transactionHandle = (CacheTransactionHandle)transactionHash.get(transactionID);
      if (transactionHandle == null)
      {
        ManifoldCFException ex = new ManifoldCFException("Bad transaction ID!",ManifoldCFException.GENERAL_ERROR);
        Logging.cache.error(Thread.currentThread().toString()+": invalidateKeys: "+transactionID+": "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex);
        throw ex;
      }
      // Do the invalidation right in the transaction handle.
      transactionHandle.invalidateKeys(invalidateKeys);
    }
  }

  /** Perform an invalidation.  Assume all appropriate locks are in place.
  *@param keys is the set of keys to invalidate.
  */
  protected void performInvalidation(StringSet keys)
    throws ManifoldCFException
  {

    // Finally, perform the invalidation.

    if (keys != null)
    {
      long invalidationTime = System.currentTimeMillis();
      // Loop through all keys
      Iterator iter = keys.getKeys();
      while (iter.hasNext())
      {
        String keyName = (String)iter.next();
        if (Logging.cache.isDebugEnabled())
          Logging.cache.debug(" Invalidating key = "+keyName+" as of time = "+Long.valueOf(invalidationTime).toString());
        writeSharedData(keyName,invalidationTime);
      }

      cache.invalidateKeys(keys);
    }
  }

  /** Leave the cache.  Must be paired with enterCache, above.
  *@param handle is the handle of the cache we are leaving.
  */
  public void leaveCache(ICacheHandle handle)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Leaving cacher; handle = "+handle.toString());
    }

    lockManager.leaveLocks(handle.getReadLockStrings(),null,handle.getWriteLockStrings());
  }

  // The following methods are used to communicate transaction information to the cache.

  /** Begin a cache transaction.
  * This keeps track of the relationship between objects cached within transactions.
  *@param startingTransactionID is the id of the transaction that is starting.
  *@param enclosingTransactionID is the id of the transaction that is in effect, or null.
  */
  public void startTransaction(String startingTransactionID, String enclosingTransactionID)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(Thread.currentThread().toString()+": Starting transaction: "+startingTransactionID+": "+this.toString()+": "+transactionHash.toString());
    }

    CacheTransactionHandle parent;
    if (enclosingTransactionID == null)
      parent = null;
    else
    {
      parent = (CacheTransactionHandle)transactionHash.get(enclosingTransactionID);
      if (parent == null)
      {
        ManifoldCFException ex = new ManifoldCFException("Illegal parent transaction ID: "+enclosingTransactionID,
          ManifoldCFException.GENERAL_ERROR);
        Logging.cache.error(Thread.currentThread().toString()+": startTransaction: "+this.toString()+": Transaction hash = "+transactionHash.toString(),ex);
        throw ex;
      }
    }

    transactionHash.put(startingTransactionID,new CacheTransactionHandle(parent));

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug("Successfully created transaction: "+startingTransactionID+": "+this.toString()+": "+transactionHash.toString());
    }

  }

  /** Commit a cache transaction.
  * This method MUST be called when a transaction successfully ends, or open locks will not be closed!!!
  * All cache activity that has taken place inside the transaction will be resolved, and the cache locks
  * held open will be released.
  */
  public void commitTransaction(String transactionID)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(Thread.currentThread().toString()+": Committing transaction: "+transactionID+": "+this.toString()+": "+transactionHash.toString());
    }

    CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID);
    if (handle == null)
      throw new ManifoldCFException("Cache manager: commit transaction without start!",ManifoldCFException.GENERAL_ERROR);

    // First, move all the locally cached entries into the global cache.
    // This is safe to do because we know that the transaction belongs to a single thread.
    CacheTransactionHandle parentTransaction = handle.getParentTransaction();
    Iterator iter = handle.getCurrentObjects();
    StringSet invalidationKeys = handle.getInvalidationKeys();
    if (parentTransaction == null)
    {
      // It will be folded into the main cache!
      // First, trigger invalidation.
      performInvalidation(invalidationKeys);
      long currentTime = System.currentTimeMillis();
      // Now, copy the objects into the main cache
      while (iter.hasNext())
      {
        ICacheDescription desc = (ICacheDescription)iter.next();
        Object o = handle.lookupObject(desc);
        // System.out.println("Moving object to main cache: "+desc.getCriticalSectionName());
        cache.setObject(desc,o,desc.getObjectKeys(),currentTime);
        // Now, set expiration and LRU
        // Update the expiration time for this object.
        resetObjectExpiration(desc,currentTime);
      }
      // End all of the locks.
      // We always end the write locks before the read locks.

      if (Logging.lock.isDebugEnabled())
        Logging.lock.debug("Ending transaction write locks for transaction "+transactionID);

      lockManager.leaveLocks(null,null,handle.getWriteLocks().getArray(cacheLockPrefix));

      if (Logging.lock.isDebugEnabled())
        Logging.lock.debug("Ending transaction read locks for transaction "+transactionID);

      lockManager.leaveLocks(handle.getReadLocks().getArray(cacheLockPrefix),null,null);

      if (Logging.lock.isDebugEnabled())
        Logging.lock.debug("Done ending "+transactionID);

    }
    else
    {
      // Copy the invalidation etc. into the wrapping transaction.
      // Invalidate the parent cache entries
      parentTransaction.invalidateKeys(invalidationKeys);
      // Copy the objects from child to parent
      while (iter.hasNext())
      {
        ICacheDescription desc = (ICacheDescription)iter.next();
        Object o = handle.lookupObject(desc);
        parentTransaction.saveObject(desc,o,desc.getObjectKeys());
      }
      // Transfer all locks to parent from child
      parentTransaction.addLocks(handle.getReadLocks(),handle.getWriteLocks());
    }

    // Finally, remove the transaction from the hash.
    transactionHash.remove(transactionID);
  }

  /** Roll back a cache transaction.
  * This method releases all objects cached against the ending transaction ID, and releases all locks
  * held for the transaction.
  */
  public void rollbackTransaction(String transactionID)
    throws ManifoldCFException
  {

    if (Logging.cache.isDebugEnabled())
    {
      Logging.cache.debug(Thread.currentThread().toString()+": Rolling back transaction: "+transactionID+": "+this.toString()+": "+transactionHash.toString());
    }

    CacheTransactionHandle handle = (CacheTransactionHandle)transactionHash.get(transactionID);
    if (handle == null)
      throw new ManifoldCFException("Cache manager: rollback transaction without start!",ManifoldCFException.GENERAL_ERROR);

    // End all of the locks
    // We always end the write locks before the read locks.

    if (Logging.lock.isDebugEnabled())
      Logging.lock.debug("Ending rollback write locks for transaction "+transactionID);

    lockManager.leaveLocks(null,null,handle.getWriteLocks().getArray(cacheLockPrefix));

    if (Logging.lock.isDebugEnabled())
      Logging.lock.debug("Ending rollback read locks for transaction "+transactionID);

    lockManager.leaveLocks(handle.getReadLocks().getArray(cacheLockPrefix),null,null);

    if (Logging.lock.isDebugEnabled())
      Logging.lock.debug("Done rolling back "+transactionID);


    // Now, get rid of the transaction entry, because we just want to chuck what we filed there.
    transactionHash.remove(transactionID);
  }

  /** Timed invalidation.  Call this periodically to get rid of all objects that have expired.
  *@param currentTimestamp is the current time in milliseconds since epoch.
  */
  public void expireObjects(long currentTimestamp)
    throws ManifoldCFException
  {
    // This is a local JVM operation; we will not need to do any locks.  We just
    // need to blow expired objects from the cache.
    cache.expireRecords(currentTimestamp);
  }


  // Protected methods and classes

  /** Read an invalidation file contents.
  *@param key is the cache key name.
  *@return the invalidation time, or 0 if none.
  */
  protected long readSharedData(String key)
    throws ManifoldCFException
  {
    // Read cache resource
    byte[] cacheResourceData = lockManager.readData("cache-"+key);
    if (cacheResourceData == null)
      return 0L;

    String expiration = new String(cacheResourceData, StandardCharsets.UTF_8);
    return Long.valueOf(expiration).longValue();


  }

  /** Write the invalidation file contents.
  *@param key is the cache key name.
  *@param value is the invalidation timestamp.
  */
  protected void writeSharedData(String key, long value)
    throws ManifoldCFException
  {
    if (value == 0L)
      lockManager.writeData(key,null);
    else
    {
      lockManager.writeData("cache-"+key,Long.toString(value).getBytes(StandardCharsets.UTF_8));
    }
  }


  /** This is the local implementation of ICacheHandle.
  */
  protected class CacheHandle implements ICacheHandle
  {
    // Member variables
    protected String[] readLocks;
    protected String[] writeLocks;
    protected ICacheDescription[] objectDescriptions;
    protected StringSet invalidationKeys;
    protected String transactionID;

    public CacheHandle(String[] readLocks, String[] writeLocks, ICacheDescription[] descriptions,
      StringSet invalidationKeys, String transactionID)
    {
      this.readLocks = readLocks;
      this.writeLocks = writeLocks;
      this.objectDescriptions = descriptions;
      this.invalidationKeys = invalidationKeys;
      this.transactionID = transactionID;
    }

    /** Get the read lock strings.
    *@return read lock string array.
    */
    public String[] getReadLockStrings()
    {
      return readLocks;
    }

    /** Get the write lock strings.
    *@return write lock string array.
    */
    public String[] getWriteLockStrings()
    {
      return writeLocks;
    }

    /** Get the set of object descriptions.
    *@return the object descriptions.
    */
    public ICacheDescription[] getObjectDescriptions()
    {
      return objectDescriptions;
    }

    /** Get the invalidation keys.
    *@return the invalidation key set.
    */
    public StringSet getInvalidationKeys()
    {
      return invalidationKeys;
    }

    /** Get the transaction ID.
    *@return the transaction ID.
    */
    public String getTransactionID()
    {
      return transactionID;
    }
  }

  /** This is the local implementation of ICacheCreateHandle
  */
  protected class CacheCreateHandle implements ICacheCreateHandle
  {
    // Member variables
    protected String[] criticalSectionNames;
    protected long theTime;
    protected String transactionID;

    public CacheCreateHandle(String[] criticalSectionNames, String transactionID)
    {
      this.criticalSectionNames = criticalSectionNames;
      theTime = System.currentTimeMillis();
      this.transactionID = transactionID;
    }

    /** Get the critical section names.
    *@return the critical section names.
    */
    public String[] getCriticalSectionNames()
    {
      return criticalSectionNames;
    }

    /** Get create start time.
    *@return the time in milliseconds since epoch.
    */
    public long getLookupTime()
    {
      return theTime;
    }

    /** Get the transaction ID.
    *@return the transaction ID.
    */
    public String getTransactionID()
    {
      return transactionID;
    }

  }

  /** This is the class the cache manager uses to keep track of transaction
  * relationships and data.  Since a transaction is local to a thread, this
  * class is local to a specific thread as well; it is thus referenced by a thread-local
  * hash table.
  */
  protected class CacheTransactionHandle
  {
    // Member variables
    protected CacheTransactionHandle parentTransaction;

    // The cache in here must also be flushable via cache keys; the
    // objects are therefore in a cache key section as well as in a "lookup" section.

    /** This is the object hash (key is the description object, value is the stored object).
    */
    protected HashMap objectHash = new HashMap();

    /** This is the cache key map (key is the cache key, value is a hashmap containing the
    * object descriptions to be invalidated).
    */
    protected HashMap cacheKeyMap = new HashMap();

    /** This is the set of cache key read locks that are currently held by this transaction.
    */
    protected HashMap cacheKeyReadLocks = new HashMap();

    /** This is the set of cache key write locks that are currently held by this transaction.
    */
    protected HashMap cacheKeyWriteLocks = new HashMap();

    /** This is the current set of keys to invalidate if the transaction should be committed.
    */
    protected StringSetBuffer invalidationKeys = new StringSetBuffer();

    /** Constructor.
    *@param parentTransaction is the parent transaction identifier, or null if none.
    */
    public CacheTransactionHandle(CacheTransactionHandle parentTransaction)
    {
      this.parentTransaction = parentTransaction;
    }

    /** Get parent transaction.
    *@return the parent transaction.
    */
    public CacheTransactionHandle getParentTransaction()
    {
      return parentTransaction;
    }

    /** Get the set of write locks to close on exit from this transaction.
    *@return the write lock names as an array.
    */
    public StringSet getWriteLocks()
    {
      return new StringSet(cacheKeyWriteLocks);
    }

    /** Get the set of read locks to close on exit from this transaction.
    *@return the read lock names as an array.
    */
    public StringSet getReadLocks()
    {
      return new StringSet(cacheKeyReadLocks);
    }

    /** Get the current invalidation keys.
    *@return the keys as a stringset
    */
    public StringSet getInvalidationKeys()
    {
      return new StringSet(invalidationKeys);
    }

    /** Look for an object in cache.
    *@param descriptionObject is the cache description object.
    *@return the object, if found, or null if not found.
    */
    public Object lookupObject(ICacheDescription descriptionObject)
    {
      return objectHash.get(descriptionObject);
    }

    /** Save an object in cache.
    *@param descriptionObject is the description.
    *@param object is the object to save.
    *@param cacheKeys are the cache keys.
    */
    public void saveObject(ICacheDescription descriptionObject, Object object,
      StringSet cacheKeys)
    {
      // Put into main hash
      objectHash.put(descriptionObject,object);
      // Put into the cache key hashes
      Iterator iter = cacheKeys.getKeys();
      while (iter.hasNext())
      {
        String key = (String)iter.next();
        HashMap thisHash = (HashMap)cacheKeyMap.get(key);
        if (thisHash == null)
        {
          thisHash = new HashMap();
          cacheKeyMap.put(key,thisHash);
        }
        thisHash.put(descriptionObject,descriptionObject);
      }
    }

    /** Invalidate objects.
    *@param keys is the set of keys to invalidate.
    */
    public void invalidateKeys(StringSet keys)
    {
      if (keys == null)
        return;

      // Merge these keys into the invalidation set
      invalidationKeys.add(keys);
      // Now, bump stuff from the cache
      Iterator iter = keys.getKeys();
      while (iter.hasNext())
      {
        String keyName = (String)iter.next();
        HashMap x = (HashMap)cacheKeyMap.get(keyName);
        if (x != null)
        {
          // Need to flush the stated items
          Iterator iter2 = x.keySet().iterator();
          while (iter2.hasNext())
          {
            ICacheDescription desc = (ICacheDescription)iter2.next();
            objectHash.remove(desc);
          }
          cacheKeyMap.remove(keyName);
        }
      }
    }

    /** See if cache keys intersect with invalidation keys.
    *@param cacheKeys is the set of cache keys that describe an object.
    *@return true if these keys have been invalidated in this transaction
    */
    public boolean checkCacheKeys(StringSet cacheKeys)
    {
      return invalidationKeys.contains(cacheKeys);
    }

    /** Come up with the set of read locks we still need to throw.
    *@param cacheKeys is the set of cache keys we need to have read locks for.
    *@param keys is the set of invalidation keys we need to have write locks for.
    *@return the array of read locks we still need to throw.
    */
    public StringSet getRemainingReadLocks(StringSet cacheKeys, StringSet keys)
    {
      StringSetBuffer accumulator = new StringSetBuffer();
      if (cacheKeys != null)
      {
        Iterator iter = cacheKeys.getKeys();
        while (iter.hasNext())
        {
          String cacheKey = (String)iter.next();
          // If this already has a lock, of any kind, continue
          if (keys != null && keys.contains(cacheKey))
            continue;
          // Look back through the whole parent chain to see whether we need to throw
          // this lock.
          CacheTransactionHandle ptr = this;
          boolean found = false;
          while (ptr != null)
          {
            if (ptr.cacheKeyReadLocks.get(cacheKey) != null ||
              ptr.cacheKeyWriteLocks.get(cacheKey) != null)
            {
              found = true;
              break;
            }
            ptr = ptr.parentTransaction;
          }
          if (found)
            continue;
          accumulator.add(cacheKey);
        }
      }
      return new StringSet(accumulator);
    }

    /** Come up with the set of write locks we still need to throw.
    *@param cacheKeys is the set of cache keys we need to have read locks for.
    *@param keys is the set of invalidation keys we need to have write locks for.
    *@return the array of write locks we still need to throw.
    */
    public StringSet getRemainingWriteLocks(StringSet cacheKeys, StringSet keys)
      throws ManifoldCFException
    {
      // If any of these keys are read locks but not yet write locks, we throw an exception!
      // (There is currently no ability to promote a read lock to a write lock.)
      StringSetBuffer accumulator = new StringSetBuffer();
      if (keys != null)
      {
        Iterator iter = keys.getKeys();
        while (iter.hasNext())
        {
          String invKey = (String)iter.next();
          // Look back through the whole parent chain to see whether we need to throw
          // this lock.
          CacheTransactionHandle ptr = this;
          boolean found = false;
          while (ptr != null)
          {
            // Write check first!  Then, read check.
            if (ptr.cacheKeyWriteLocks.get(invKey) != null)
            {
              found = true;
              break;
            }
            // Upgrade lock attempts are now permitted.
            if (ptr.cacheKeyReadLocks.get(invKey) != null)
              break;
            ptr = ptr.parentTransaction;
          }
          if (found)
            continue;
          accumulator.add(invKey);
        }
      }
      return new StringSet(accumulator);
    }

    /** Add to the set of locks that are open.
    *@param thrownReadLocks is the set of read locks.
    *@param thrownWriteLocks is the set of write locks.
    */
    public void addLocks(StringSet thrownReadLocks, StringSet thrownWriteLocks)
    {
      // Some of the write locks will be upgrades.  In this case, the same lock
      // might appear BOTH as read and as write.  I hope the lock manager does the
      // right thing in this case!
      // In any case, at worst we will have one write lock and one read lock for the
      // same lock name, and never a write lock then a read lock, so we should be able to disambiguate if
      // needed.

      Iterator iter = thrownReadLocks.getKeys();
      while (iter.hasNext())
      {
        String x = (String)iter.next();
        cacheKeyReadLocks.put(x,x);
      }
      iter = thrownWriteLocks.getKeys();
      while (iter.hasNext())
      {
        String x = (String)iter.next();
        cacheKeyWriteLocks.put(x,x);
      }
    }

    /** Get all existing object descriptions.
    *@return an iterator of ICacheDescription objects.
    */
    public Iterator getCurrentObjects()
    {
      return objectHash.keySet().iterator();
    }

  }


}
