/** 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;
    }
  }
  /**
   * Transaction-long setting to force all the share cache to be bypassed for the current
   * transaction.
   *
   * <p>This setting is like having a {@link NullCache null} {@link #setSharedCache(SimpleCache)
   * shared cache}, but only lasts for the transaction.
   *
   * <p>Use this when a read transaction <b>must</b> see consistent and current data i.e. go to the
   * database. While this is active, write operations will also not be committed to the shared
   * cache.
   *
   * @param noSharedCacheRead <tt>true</tt> to avoid reading from the shared cache for the
   *     transaction
   */
  @SuppressWarnings("unchecked")
  public void setDisableSharedCacheReadForTransaction(boolean noSharedCacheRead) {
    TransactionData txnData = getTransactionData();

    // If we are switching on noSharedCacheRead mode, convert all existing reads and updates to
    // avoid 'consistent
    // read' behaviour giving us a potentially out of date node already accessed
    if (noSharedCacheRead && !txnData.noSharedCacheRead) {
      txnData.noSharedCacheRead = noSharedCacheRead;
      String currentCacheRegion = TenantUtil.getCurrentDomain();
      for (Map.Entry<Serializable, CacheBucket<V>> entry :
          new ArrayList<Map.Entry<Serializable, CacheBucket<V>>>(
              txnData.updatedItemsCache.entrySet())) {
        Serializable cacheKey = entry.getKey();
        K key = null;
        if (cacheKey instanceof CacheRegionKey) {
          CacheRegionKey cacheRegionKey = (CacheRegionKey) cacheKey;
          if (currentCacheRegion.equals(cacheRegionKey.getCacheRegion())) {
            key = (K) cacheRegionKey.getCacheKey();
          }
        } else {
          key = (K) cacheKey;
        }

        if (key != null) {
          CacheBucket<V> bucket = entry.getValue();
          // Simply 'forget' reads
          if (bucket instanceof ReadCacheBucket) {
            txnData.updatedItemsCache.remove(cacheKey);
          }
          // Convert updates to removes
          else if (bucket instanceof UpdateCacheBucket) {
            remove(key);
          }
          // Leave new entries alone - they can't have come from the shared cache
        }
      }
    }
  }
  /** Clears out all the caches. */
  public void clear() {
    // clear local caches
    if (AlfrescoTransactionSupport.getTransactionId() != null) {
      if (isDebugEnabled) {
        logger.debug(
            "In transaction clearing cache: \n"
                + "   cache: "
                + this
                + "\n"
                + "   txn: "
                + AlfrescoTransactionSupport.getTransactionId());
      }

      TransactionData txnData = getTransactionData();
      // Ensure that the cache isn't being modified
      if (txnData.isClosed) {
        if (isDebugEnabled) {
          logger.debug("In post-commit clear: \n" + "   cache: " + this);
        }
      } else {
        // The shared cache must be cleared at the end of the transaction
        // and also serves to ensure that the shared cache will be ignored
        // for the remainder of the transaction.
        // We do, however, keep all locked values locked.
        txnData.isClearOn = true;
        txnData.updatedItemsCache.clear();
        txnData.removedItemsCache.clear();
      }
    } else // no transaction
    {
      if (isDebugEnabled) {
        logger.debug("No transaction - clearing shared cache");
      }
      // clear shared cache
      sharedCache.clear();
    }
  }
  /** To be used in a transaction only. */
  private TransactionData getTransactionData() {
    @SuppressWarnings("unchecked")
    TransactionData data =
        (TransactionData) AlfrescoTransactionSupport.getResource(resourceKeyTxnData);
    if (data == null) {
      data = new TransactionData();
      // create and initialize caches
      data.updatedItemsCache = new LRULinkedHashMap<Serializable, CacheBucket<V>>(23);
      data.removedItemsCache = new HashSet<Serializable>(13);
      data.lockedItemsCache = new HashSet<Serializable>(13);
      data.isReadOnly =
          AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY;
      data.stats = new TransactionStats();

      // ensure that we get the transaction callbacks as we have bound the unique
      // transactional caches to a common manager
      AlfrescoTransactionSupport.bindListener(this);
      AlfrescoTransactionSupport.bindResource(resourceKeyTxnData, data);
    }
    return data;
  }
  /**
   * Goes direct to the shared cache in the absence of a transaction.
   *
   * <p>Where a transaction is present, a cache of removed items is lazily added to the thread and
   * the <tt>Object</tt> put onto that.
   */
  public void remove(K keyIn) {
    final Serializable key = getTenantAwareCacheKey(keyIn);

    // are we in a transaction?
    if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction
    {
      // no transaction
      sharedCache.remove(key);
      // done
      if (isDebugEnabled) {
        logger.debug(
            "No transaction - removing item from shared cache: \n"
                + "   cache: "
                + this
                + "\n"
                + "   key: "
                + key);
      }
    } else // transaction present
    {
      TransactionData txnData = getTransactionData();
      // Ensure that the cache isn't being modified
      if (txnData.isClosed) {
        if (isDebugEnabled) {
          logger.debug("In post-commit remove: \n" + "   cache: " + this + "\n" + "   key: " + key);
        }
      } else if (isValueLocked(txnData, key)) {
        // The key has been locked
        if (isDebugEnabled) {
          logger.debug(
              "Ignoring remove after detecting locked key: \n"
                  + "   cache: "
                  + this
                  + "\n"
                  + "   key: "
                  + key);
        }
      } else {
        // is the shared cache going to be cleared?
        if (txnData.isClearOn) {
          // don't store removals if we're just going to clear it all out later
        } else {
          // are we in an overflow condition?
          if (txnData.removedItemsCache.size() >= maxCacheSize) {
            // overflow about to occur or has occured - we can only guarantee non-stale
            // data by clearing the shared cache after the transaction.  Also, the
            // shared cache needs to be ignored for the rest of the transaction.
            txnData.isClearOn = true;
            if (!txnData.haveIssuedFullWarning) {
              if (logger.isInfoEnabled()) {
                Exception e = new Exception("Stack: ");
                logger.info(
                    "Transactional removal cache '" + name + "' is full (" + maxCacheSize + ").",
                    e);
              } else if (logger.isWarnEnabled()) {
                logger.warn(
                    "Transactional removal cache '" + name + "' is full (" + maxCacheSize + ").");
              }
              txnData.haveIssuedFullWarning = true;
            }
          } else {
            // Create a bucket to remove the value from the shared cache
            txnData.removedItemsCache.add(key);
          }
        }
        // remove the item from the udpated cache, if present
        txnData.updatedItemsCache.remove(key);
        // done
        if (isDebugEnabled) {
          logger.debug(
              "In transaction - adding item direct to transactional removed cache: \n"
                  + "   cache: "
                  + this
                  + "\n"
                  + "   key: "
                  + key);
        }
      }
    }
  }
  /**
   * Goes direct to the shared cache in the absence of a transaction.
   *
   * <p>Where a transaction is present, a cache of updated items is lazily added to the thread and
   * the <tt>Object</tt> put onto that.
   */
  public void put(K keyIn, V value) {
    final Serializable key = getTenantAwareCacheKey(keyIn);

    // are we in a transaction?
    if (AlfrescoTransactionSupport.getTransactionId() == null) // not in transaction
    {
      // no transaction
      TransactionalCache.putSharedCacheValue(sharedCache, key, value, null);
      // done
      if (isDebugEnabled) {
        logger.debug(
            "No transaction - adding item direct to shared cache: \n"
                + "   cache: "
                + this
                + "\n"
                + "   key: "
                + key
                + "\n"
                + "   value: "
                + value);
      }
    } else // transaction present
    {
      TransactionData txnData = getTransactionData();
      // Ensure that the cache isn't being modified
      if (txnData.isClosed) {
        if (isDebugEnabled) {
          logger.debug(
              "In post-commit add: \n"
                  + "   cache: "
                  + this
                  + "\n"
                  + "   key: "
                  + key
                  + "\n"
                  + "   value: "
                  + value);
        }
      } else if (isValueLocked(txnData, key)) {
        // The key has been locked
        if (isDebugEnabled) {
          logger.debug(
              "Ignoring put after detecting locked key: \n"
                  + "   cache: "
                  + this
                  + "\n"
                  + "   key: "
                  + key
                  + "\n"
                  + "   value: "
                  + value);
        }
      } else {
        // we have an active transaction - add the item into the updated cache for this transaction
        // are we in an overflow condition?
        if (txnData.updatedItemsCache.hasHitSize()) {
          // overflow about to occur or has occured - we can only guarantee non-stale
          // data by clearing the shared cache after the transaction.  Also, the
          // shared cache needs to be ignored for the rest of the transaction.
          txnData.isClearOn = true;
          if (!txnData.haveIssuedFullWarning) {
            if (logger.isInfoEnabled()) {
              Exception e = new Exception("Stack: ");
              logger.info(
                  "Transactional update cache '" + name + "' is full (" + maxCacheSize + ").", e);
            } else if (logger.isWarnEnabled()) {
              logger.warn(
                  "Transactional update cache '" + name + "' is full (" + maxCacheSize + ").");
            }
            txnData.haveIssuedFullWarning = true;
          }
        }
        ValueHolder<V> existingValueHolder =
            txnData.noSharedCacheRead ? null : sharedCache.get(key);
        CacheBucket<V> bucket = null;
        if (existingValueHolder == null) {
          // ALF-5134: Performance of Alfresco cluster less than performance of single node
          // The 'null' marker that used to be inserted also triggered an update in the afterCommit
          // phase; the update triggered cache invalidation in the cluster.  Now, the null cannot
          // be verified to be the same null - there is no null equivalence
          //
          // The value didn't exist before
          bucket = new NewCacheBucket<V>(value);
        } else {
          // Record the existing value as is
          bucket = new UpdateCacheBucket<V>(existingValueHolder, value);
        }
        txnData.updatedItemsCache.put(key, bucket);
        // remove the item from the removed cache, if present
        txnData.removedItemsCache.remove(key);
        // done
        if (isDebugEnabled) {
          logger.debug(
              "In transaction - adding item direct to transactional update cache: \n"
                  + "   cache: "
                  + this
                  + "\n"
                  + "   key: "
                  + key
                  + "\n"
                  + "   value: "
                  + value);
        }
      }
    }
  }
 /**
  * Ensures that the transactional caches are removed from the common cache manager.
  *
  * @param txnData the data with references to the the transactional caches
  */
 private void removeCaches(TransactionData txnData) {
   txnData.isClosed = true;
 }