/** Merge the transactional caches into the shared cache */
  public void beforeCommit(boolean readOnly) {
    if (isDebugEnabled) {
      logger.debug("Processing before-commit");
    }

    TransactionData txnData = getTransactionData();
    try {
      if (txnData.isClearOn) {
        // clear shared cache
        final long startNanos = cacheStatsEnabled ? System.nanoTime() : 0;
        sharedCache.clear();
        final long endNanos = cacheStatsEnabled ? System.nanoTime() : 0;
        if (cacheStatsEnabled) {
          TransactionStats stats = txnData.stats;
          stats.record(startNanos, endNanos, OpType.CLEAR);
        }
        if (isDebugEnabled) {
          logger.debug("Clear notification recieved in commit - clearing shared cache");
        }
      } else {
        // transfer any removed items
        for (Serializable key : txnData.removedItemsCache) {
          final long startNanos = System.nanoTime();
          sharedCache.remove(key);
          final long endNanos = System.nanoTime();
          TransactionStats stats = txnData.stats;
          stats.record(startNanos, endNanos, OpType.REMOVE);
        }
        if (isDebugEnabled) {
          logger.debug(
              "Removed "
                  + txnData.removedItemsCache.size()
                  + " values from shared cache in commit");
        }
      }

      // transfer updates
      Set<Serializable> keys = (Set<Serializable>) txnData.updatedItemsCache.keySet();
      for (Map.Entry<Serializable, CacheBucket<V>> entry :
          (Set<Map.Entry<Serializable, CacheBucket<V>>>) txnData.updatedItemsCache.entrySet()) {
        Serializable key = entry.getKey();
        CacheBucket<V> bucket = entry.getValue();
        bucket.doPreCommit(
            sharedCache, key, this.isMutable, this.allowEqualsChecks, txnData.isReadOnly);
      }
      if (isDebugEnabled) {
        logger.debug("Pre-commit called for " + keys.size() + " values.");
      }
    } catch (Throwable e) {
      throw new AlfrescoRuntimeException("Failed to transfer updates to shared cache", e);
    } finally {
      // Block any further updates
      txnData.isClosed = true;
    }
  }
  /** Merge the transactional caches into the shared cache */
  public void afterCommit() {
    if (isDebugEnabled) {
      logger.debug("Processing after-commit");
    }

    TransactionData txnData = getTransactionData();
    try {
      if (txnData.isClearOn) {
        // clear shared cache
        final long startNanos = cacheStatsEnabled ? System.nanoTime() : 0;
        sharedCache.clear();
        final long endNanos = cacheStatsEnabled ? System.nanoTime() : 0;
        if (cacheStatsEnabled) {
          TransactionStats stats = txnData.stats;
          stats.record(startNanos, endNanos, OpType.CLEAR);
        }
        if (isDebugEnabled) {
          logger.debug("Clear notification recieved in commit - clearing shared cache");
        }
      } else {
        // transfer any removed items
        for (Serializable key : txnData.removedItemsCache) {
          final long startNanos = System.nanoTime();
          sharedCache.remove(key);
          final long endNanos = System.nanoTime();
          TransactionStats stats = txnData.stats;
          stats.record(startNanos, endNanos, OpType.REMOVE);
        }
        if (isDebugEnabled) {
          logger.debug(
              "Removed "
                  + txnData.removedItemsCache.size()
                  + " values from shared cache in commit");
        }
      }

      // transfer updates
      Set<Serializable> keys = (Set<Serializable>) txnData.updatedItemsCache.keySet();
      for (Map.Entry<Serializable, CacheBucket<V>> entry :
          (Set<Map.Entry<Serializable, CacheBucket<V>>>) txnData.updatedItemsCache.entrySet()) {
        Serializable key = entry.getKey();
        CacheBucket<V> bucket = entry.getValue();
        try {
          bucket.doPostCommit(
              sharedCache,
              key,
              this.isMutable,
              this.allowEqualsChecks,
              txnData.isReadOnly,
              txnData.stats);
        } catch (Exception e) {
          // MNT-10486: NPE in NodeEntity during post-commit write through to shared cache
          //              This try-catch is diagnostic in nature.  We need to know the names of the
          // caches
          //              and details of the values involved.
          //              The causal exception will be rethrown.
          throw new AlfrescoRuntimeException(
              "CacheBucket postCommit transfer to shared cache failed: \n"
                  + "   Cache:      "
                  + sharedCache
                  + "\n"
                  + "   Key:        "
                  + key
                  + "\n"
                  + "   New Value:  "
                  + bucket.getValue()
                  + "\n"
                  + "   Cache Value:"
                  + sharedCache.get(key),
              e);
        }
      }
      if (isDebugEnabled) {
        logger.debug("Post-commit called for " + keys.size() + " values.");
      }
    } catch (Throwable e) {
      throw new AlfrescoRuntimeException("Failed to transfer updates to shared cache", e);
    } finally {
      removeCaches(txnData);
      // Aggregate this transaction's stats with centralised cache stats.
      if (cacheStatsEnabled) {
        cacheStats.add(name, txnData.stats);
      }
    }
  }
  /**
   * Checks the per-transaction caches for the object before going to the shared cache. If the
   * thread is not in a transaction, then the shared cache is accessed directly.
   */
  public V get(K keyIn) {
    final Serializable key = getTenantAwareCacheKey(keyIn);

    boolean ignoreSharedCache = false;
    // are we in a transaction?
    if (AlfrescoTransactionSupport.getTransactionId() != null) {
      TransactionData txnData = getTransactionData();
      if (txnData.isClosed) {
        // This check could have been done in the first if block, but that would have added another
        // call to the
        // txn resources.
      } else // The txn is still active
      {
        if (!txnData.isClearOn) // deletions cache only useful before a clear
        {
          // check to see if the key is present in the transaction's removed items
          if (txnData.removedItemsCache.contains(key)) {
            // it has been removed in this transaction
            if (isDebugEnabled) {
              logger.debug(
                  "get returning null - item has been removed from transactional cache: \n"
                      + "   cache: "
                      + this
                      + "\n"
                      + "   key: "
                      + key);
            }
            return null;
          }
        }

        // check for the item in the transaction's new/updated items
        CacheBucket<V> bucket = (CacheBucket<V>) txnData.updatedItemsCache.get(key);
        if (bucket != null) {
          V value = bucket.getValue();
          // element was found in transaction-specific updates/additions
          if (isDebugEnabled) {
            logger.debug(
                "Found item in transactional cache: \n"
                    + "   cache: "
                    + this
                    + "\n"
                    + "   key: "
                    + key
                    + "\n"
                    + "   value: "
                    + value);
          }
          return value;
        } else if (txnData.isClearOn) {
          // Can't store values in the current txn any more
          ignoreSharedCache = true;
        } else if (txnData.noSharedCacheRead) {
          // Explicitly told to ignore shared cache
          ignoreSharedCache = true;
        } else {
          // There is no in-txn entry for the key
          // Use the value direct from the shared cache
          V value = null;
          if (cacheStatsEnabled) {
            value = TransactionalCache.getSharedCacheValue(sharedCache, key, txnData.stats);
          } else {
            // No stats tracking, pass in null TransactionStats
            value = TransactionalCache.getSharedCacheValue(sharedCache, key, null);
          }
          bucket = new ReadCacheBucket<V>(value);
          txnData.updatedItemsCache.put(key, bucket);
          return value;
        }
      }
    }
    // no value found - must we ignore the shared cache?
    if (!ignoreSharedCache) {
      V value = TransactionalCache.getSharedCacheValue(sharedCache, key, null);
      // go to the shared cache
      if (isDebugEnabled) {
        logger.debug(
            "No value found in transaction - fetching instance from shared cache: \n"
                + "   cache: "
                + this
                + "\n"
                + "   key: "
                + key
                + "\n"
                + "   value: "
                + value);
      }
      return value;
    } else // ignore shared cache
    {
      if (isDebugEnabled) {
        logger.debug(
            "No value found in transaction and ignoring shared cache: \n"
                + "   cache: "
                + this
                + "\n"
                + "   key: "
                + key);
      }
      return null;
    }
  }