/*
 * Copyright (C) 2003-2010 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.services.jcr.impl.core.lock.cacheable;

import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.services.jcr.config.RepositoryConfigurationException;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.dataflow.ChangesLogIterator;
import org.exoplatform.services.jcr.dataflow.CompositeChangesLog;
import org.exoplatform.services.jcr.dataflow.DataManager;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.backup.BackupException;
import org.exoplatform.services.jcr.impl.backup.Backupable;
import org.exoplatform.services.jcr.impl.backup.DataRestore;
import org.exoplatform.services.jcr.impl.backup.DummyDataRestore;
import org.exoplatform.services.jcr.impl.backup.rdbms.DBBackup;
import org.exoplatform.services.jcr.impl.backup.rdbms.DataRestoreContext;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.lock.LockRemover;
import org.exoplatform.services.jcr.impl.core.lock.LockRemoverHolder;
import org.exoplatform.services.jcr.impl.core.lock.LockTableHandler;
import org.exoplatform.services.jcr.impl.core.lock.SessionLockManager;
import org.exoplatform.services.jcr.impl.dataflow.TransientItemData;
import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData;
import org.exoplatform.services.jcr.impl.dataflow.persistent.WorkspacePersistentDataManager;
import org.exoplatform.services.jcr.impl.storage.JCRInvalidItemStateException;
import org.exoplatform.services.jcr.observation.ExtendedEvent;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.security.IdentityConstants;
import org.exoplatform.services.transaction.ActionNonTxAware;
import org.picocontainer.Startable;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;

/**
 * Created by The eXo Platform SAS.
 * 
 * <br>Date:
 *
 * @author <a href="karpenko.sergiy@gmail.com">Karpenko Sergiy</a> 
 * @version $Id: AbstractCacheableLockManagerImpl.java 2806 2010-07-21 08:00:15Z tolusha $
 */
public abstract class AbstractCacheableLockManager implements CacheableLockManager, ItemsPersistenceListener,
   Startable, Backupable
{
   /**
    *  The name to property time out.  
    */
   public static final String TIME_OUT = "time-out";

   /**
    * Default lock time out. 30min
    */
   public static final long DEFAULT_LOCK_TIMEOUT = 1000 * 60 * 30;

   /**
    * Data manager.
    */
   protected final DataManager dataManager;

   /**
    * Run time lock time out.
    */
   protected long lockTimeOut;

   /**
    * Lock remover thread.
    */
   protected LockRemover lockRemover;

   /**
    * Workspace configuration;
    */
   protected final WorkspaceEntry config;

   /**
    * SessionLockManagers that uses this LockManager.
    */
   protected Map<String, CacheableSessionLockManager> sessionLockManagers;

   /**
    * The current Transaction Manager
    */
   protected TransactionManager tm;

   /**
    * Store datasource to have ability to get from context without recreation.
    */
   protected DataSource dataSource;

   public static final String LOCKS_FORCE_REMOVE = "org.exoplatform.jcr.locks.force.remove";

   /**
    * Logger
    */
   protected static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.AbstractCacheableLockManager");

   protected LockActionNonTxAware<Integer, Object> getNumLocks;

   protected LockActionNonTxAware<Boolean, Object> hasLocks;

   protected LockActionNonTxAware<Boolean, String> isLockLive;

   protected LockActionNonTxAware<Object, LockData> refresh;

   protected LockActionNonTxAware<Boolean, String> lockExist;

   protected LockActionNonTxAware<LockData, String> getLockDataById;

   protected LockActionNonTxAware<List<LockData>, Object> getLockList;

   /**
    * Constructor.
    * 
    * @param dataManager - workspace persistent data manager
    * @param config - workspace entry
    * @param transactionManager 
    *          the transaction manager
    * @throws RepositoryConfigurationException 
    */
   public AbstractCacheableLockManager(WorkspacePersistentDataManager dataManager, WorkspaceEntry config,
      TransactionManager transactionManager, LockRemoverHolder lockRemoverHolder)
      throws RepositoryConfigurationException
   {
      if (config.getLockManager() != null)
      {
         if (config.getLockManager().hasParameters()
            && config.getLockManager().getParameterValue(TIME_OUT, null) != null)
         {
            long timeOut = config.getLockManager().getParameterTime(TIME_OUT);
            lockTimeOut = timeOut > 0 ? timeOut : DEFAULT_LOCK_TIMEOUT;
         }
      }
      else
      {
         lockTimeOut = DEFAULT_LOCK_TIMEOUT;
      }

      this.config = config;
      this.dataManager = dataManager;
      this.sessionLockManagers = new ConcurrentHashMap<String, CacheableSessionLockManager>();
      this.tm = transactionManager;
      this.lockRemover = lockRemoverHolder.getLockRemover(this);
      dataManager.addItemPersistenceListener(this);
   }

   /**
    * Returns the number of active locks.
    */
   @Managed
   @ManagedDescription("The number of active locks")
   public int getNumLocks()
   {
      try
      {
         return getNumLocks.run();
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return -1;
   }

   /**
    * Indicates if some locks have already been created.
    */
   protected boolean hasLocks()
   {
      try
      {
         return hasLocks.run();
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return true;
   }

   /**
    * Check is LockManager contains lock. No matter it is in pending or persistent state.
    */
   public boolean isLockLive(String nodeId) throws LockException
   {
      try
      {
         return isLockLive.run(nodeId);
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return false;
   }

   /**
    * Refreshed lock data in cache
    */
   public void refreshLockData(LockData newLockData) throws LockException
   {
      refresh.run(newLockData);
   }

   /**
    * Check is LockManager contains lock. 
    */
   public boolean lockExist(String nodeId)
   {
      try
      {
         return lockExist.run(nodeId);
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return false;
   }

   /**
    * Returns lock data by node identifier.
    */
   protected LockData getLockDataById(String nodeId)
   {
      try
      {
         return getLockDataById.run(nodeId);
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return null;
   }

   /**
    * Returns all locks.
    */
   protected synchronized List<LockData> getLockList()
   {
      try
      {
         return getLockList.run();
      }
      catch (LockException e)
      {
         if (LOG.isTraceEnabled())
         {
            LOG.trace("An exception occurred: " + e.getMessage());
         }
      }
      return null;
   }

   @Managed
   @ManagedDescription("Remove the expired locks")
   public void cleanExpiredLocks()
   {
      removeExpired();
   }

   public long getDefaultLockTimeOut()
   {
      return lockTimeOut;
   }

   /**
    * Return new instance of session lock manager.
    */
   public SessionLockManager getSessionLockManager(String sessionId, SessionDataManager transientManager)
   {
      CacheableSessionLockManager sessionManager = new CacheableSessionLockManager(sessionId, this, transientManager);
      sessionLockManagers.put(sessionId, sessionManager);
      return sessionManager;
   }

   /**
    * {@inheritDoc}
    */
   public boolean isTXAware()
   {
      return true;
   }

   /**
    * {@inheritDoc}
    */
   public void onSaveItems(ItemStateChangesLog changesLog)
   {
      List<PlainChangesLog> chengesLogList = new ArrayList<PlainChangesLog>();
      if (changesLog instanceof TransactionChangesLog)
      {
         ChangesLogIterator logIterator = ((TransactionChangesLog)changesLog).getLogIterator();

         while (logIterator.hasNextLog())
         {
            chengesLogList.add(logIterator.nextLog());
         }
      }
      else if (changesLog instanceof PlainChangesLog)
      {
         chengesLogList.add((PlainChangesLog)changesLog);
      }
      else if (changesLog instanceof CompositeChangesLog)
      {
         for (ChangesLogIterator iter = ((CompositeChangesLog)changesLog).getLogIterator(); iter.hasNextLog();)
         {
            chengesLogList.add(iter.nextLog());
         }
      }

      List<LockOperationContainer> containers = new ArrayList<LockOperationContainer>();

      for (PlainChangesLog currChangesLog : chengesLogList)
      {
         String sessionId = currChangesLog.getSessionId();

         String nodeIdentifier;
         try
         {
            switch (currChangesLog.getEventType())
            {
               case ExtendedEvent.LOCK :
                  if (currChangesLog.getSize() < 2)
                  {
                     LOG.error("Incorrect changes log  of type ExtendedEvent.LOCK size=" + currChangesLog.getSize()
                        + "<2 \n" + currChangesLog.dump());
                     break;
                  }
                  nodeIdentifier = currChangesLog.getAllStates().get(0).getData().getParentIdentifier();

                  CacheableSessionLockManager session = sessionLockManagers.get(sessionId);
                  if (session != null && session.containsPendingLock(nodeIdentifier))
                  {
                     containers.add(new LockOperationContainer(nodeIdentifier, currChangesLog.getSessionId(),
                        ExtendedEvent.LOCK));
                  }
                  else
                  {
                     LOG.error("Lock must exist in pending locks.");
                  }
                  break;
               case ExtendedEvent.UNLOCK :
                  if (currChangesLog.getSize() < 2)
                  {
                     LOG.error("Incorrect changes log  of type ExtendedEvent.UNLOCK size=" + currChangesLog.getSize()
                        + "<2 \n" + currChangesLog.dump());
                     break;
                  }

                  containers.add(new LockOperationContainer(currChangesLog.getAllStates().get(0).getData()
                     .getParentIdentifier(), currChangesLog.getSessionId(), ExtendedEvent.UNLOCK));
                  break;
               default :
                  HashSet<String> removedLock = new HashSet<String>();
                  for (ItemState itemState : currChangesLog.getAllStates())
                  {
                     // this is a node and node is locked
                     if (itemState.getData().isNode() && lockExist(itemState.getData().getIdentifier()))
                     {
                        nodeIdentifier = itemState.getData().getIdentifier();
                        if (itemState.isDeleted())
                        {
                           removedLock.add(nodeIdentifier);
                        }
                        else if (itemState.isAdded() || itemState.isRenamed() || itemState.isUpdated())
                        {
                           removedLock.remove(nodeIdentifier);
                        }
                     }
                  }
                  for (String identifier : removedLock)
                  {
                     containers.add(new LockOperationContainer(identifier, currChangesLog.getSessionId(),
                        ExtendedEvent.UNLOCK));
                  }
                  break;
            }
         }
         catch (IllegalStateException e)
         {
            LOG.error(e.getLocalizedMessage(), e);
         }
      }

      // sort locking and unlocking operations to avoid deadlocks
      Collections.sort(containers);
      for (LockOperationContainer container : containers)
      {
         try
         {
            container.apply();
         }
         catch (LockException e)
         {
            LOG.error(e.getMessage(), e);
         }
      }
   }

   /**
    * Class containing operation type (LOCK or UNLOCK) and all the needed information like node uuid and session id.
    */
   private class LockOperationContainer implements Comparable<LockOperationContainer>
   {

      private String identifier;

      private String sessionId;

      private int type;

      /**
       * @param identifier node identifier 
       * @param sessionId id of session
       * @param type ExtendedEvent type specifying the operation (LOCK or UNLOCK)
       */
      public LockOperationContainer(String identifier, String sessionId, int type)
      {
         super();
         this.identifier = identifier;
         this.sessionId = sessionId;
         this.type = type;
      }

      /**
       * @return node identifier
       */
      public String getIdentifier()
      {
         return identifier;
      }

      public void apply() throws LockException
      {
         // invoke internalLock in LOCK operation
         if (type == ExtendedEvent.LOCK)
         {
            internalLock(sessionId, identifier);
         }
         // invoke internalUnLock in UNLOCK operation
         else if (type == ExtendedEvent.UNLOCK)
         {
            internalUnLock(sessionId, identifier);
         }
      }

      /**
       * @see java.lang.Comparable#compareTo(java.lang.Object)
       */
      public int compareTo(LockOperationContainer o)
      {
         return identifier.compareTo(o.getIdentifier());
      }
   }

   /**
    * Remove expired locks. Used from LockRemover.
    */
   public synchronized void removeExpired()
   {
      final List<String> removeLockList = new ArrayList<String>();

      for (LockData lock : getLockList())
      {
         if (!lock.isSessionScoped() && lock.getTimeToDeath() < 0)
         {
            removeLockList.add(lock.getNodeIdentifier());
         }
      }

      Collections.sort(removeLockList);

      for (String rLock : removeLockList)
      {
         removeLock(rLock);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void start()
   {
      lockRemover.start();

      // Remove all locks records directly from DB. 
      boolean deleteLocks = "true".equalsIgnoreCase(System.getProperty(LOCKS_FORCE_REMOVE, "false"));

      if (deleteLocks)
      {
         doClean();
      }

      // remove all locks at the start up time EXOJCR-1592
      //      if (isCoordinator())
      //      {
      //         removeAll();
      //      }
   }

   /**
    * {@inheritDoc}
    */
   public void stop()
   {
      lockRemover.stop();
      sessionLockManagers.clear();
   }

   /**
    * Copy <code>PropertyData prop</code> to new TransientItemData
    * 
    * @param prop
    * @return
    * @throws RepositoryException
    */
   protected TransientItemData copyItemData(PropertyData prop) throws RepositoryException
   {
      if (prop == null)
      {
         return null;
      }

      // make a copy, value may be null for deleting items
      TransientPropertyData newData =
         new TransientPropertyData(prop.getQPath(), prop.getIdentifier(), prop.getPersistedVersion(), prop.getType(),
            prop.getParentIdentifier(), prop.isMultiValued(), prop.getValues());

      return newData;
   }

   /**
    * Internal lock
    * 
    * @param nodeIdentifier
    * @throws LockException
    */
   protected abstract void internalLock(String sessionId, String nodeIdentifier) throws LockException;

   /**
    * Internal unlock.
    * 
    * @param sessionId
    * @param nodeIdentifier
    * @throws LockException
    */
   protected abstract void internalUnLock(String sessionId, String nodeIdentifier) throws LockException;


   /**
    * {@inheritDoc}
    */
   public String getLockTokenHash(String token)
   {
      String hash = "";
      try
      {
         MessageDigest m = MessageDigest.getInstance("MD5");
         m.update(token.getBytes(), 0, token.length());
         hash = new BigInteger(1, m.digest()).toString(16);
      }
      catch (NoSuchAlgorithmException e)
      {
         LOG.error("Can't get instanse of MD5 MessageDigest!", e);
      }
      return hash;
   }

   /**
    * {@inheritDoc}
    */
   public LockData getExactNodeOrCloseParentLock(NodeData node) throws RepositoryException
   {
      return getExactNodeOrCloseParentLock(node, true);
   }

   private LockData getExactNodeOrCloseParentLock(NodeData node, boolean checkHasLocks) throws RepositoryException
   {

      if (node == null || (checkHasLocks && !hasLocks()))
      {
         return null;
      }
      LockData retval = null;
      retval = getLockDataById(node.getIdentifier());
      if (retval == null)
      {
         NodeData parentData = (NodeData)dataManager.getItemData(node.getParentIdentifier());
         if (parentData != null)
         {
            retval = getExactNodeOrCloseParentLock(parentData, false);
         }
      }
      return retval;
   }

   /**
    * {@inheritDoc}
    */
   public LockData getExactNodeLock(NodeData node) throws RepositoryException
   {
      if (node == null || !hasLocks())
      {
         return null;
      }

      return getLockDataById(node.getIdentifier());
   }

   /**
    * {@inheritDoc}
    */
   public LockData getClosedChild(NodeData node) throws RepositoryException
   {
      return getClosedChild(node, true);
   }

   private LockData getClosedChild(NodeData node, boolean checkHasLocks) throws RepositoryException
   {

      if (node == null || (checkHasLocks && !hasLocks()))
      {
         return null;
      }
      LockData retval = null;

      List<NodeData> childData = dataManager.getChildNodesData(node);
      for (NodeData nodeData : childData)
      {
         retval = getLockDataById(nodeData.getIdentifier());
         if (retval != null)
         {
            return retval;
         }
      }
      // child not found try to find dipper
      for (NodeData nodeData : childData)
      {
         retval = getClosedChild(nodeData, false);
         if (retval != null)
         {
            return retval;
         }
      }
      return retval;
   }



   /**
    * Remove lock, used by Lock remover.
    * 
    * @param nodeIdentifier String
    */
   protected void removeLock(String nodeIdentifier)
   {
      try
      {
         NodeData nData = (NodeData)dataManager.getItemData(nodeIdentifier);

         //Skip removing, because that node was removed in other node of cluster.  
         if (nData == null)
         {
            return;
         }

         PlainChangesLog changesLog =
            new PlainChangesLogImpl(new ArrayList<ItemState>(), IdentityConstants.SYSTEM, ExtendedEvent.UNLOCK);

         ItemData lockOwner =
            copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKOWNER, 1),
               ItemType.PROPERTY));

         //Skip removing, because that lock was removed in other node of cluster.  
         if (lockOwner == null)
         {
            return;
         }

         changesLog.add(ItemState.createDeletedState(lockOwner));

         ItemData lockIsDeep =
            copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKISDEEP, 1),
               ItemType.PROPERTY));

         //Skip removing, because that lock was removed in other node of cluster.  
         if (lockIsDeep == null)
         {
            return;
         }

         changesLog.add(ItemState.createDeletedState(lockIsDeep));

         // lock probably removed by other thread
         if (lockOwner == null && lockIsDeep == null)
         {
            return;
         }

         dataManager.save(new TransactionChangesLog(changesLog));
      }
      catch (JCRInvalidItemStateException e)
      {
         //Skip property not found in DB, because that lock property was removed in other node of cluster.
         if (LOG.isDebugEnabled())
         {
            LOG.debug("The propperty was removed in other node of cluster.", e);
         }

      }
      catch (RepositoryException e)
      {
         LOG.error("Error occur during removing lock" + e.getLocalizedMessage(), e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void closeSessionLockManager(String sessionID)
   {
      sessionLockManagers.remove(sessionID);
   }

   /**
    * Actions that are not supposed to be called within a transaction
    * 
    * Created by The eXo Platform SAS
    * Author : Nicolas Filotto 
    *          nicolas.filotto@exoplatform.com
    * 21 janv. 2010
    */
   protected abstract class LockActionNonTxAware<R, A> extends ActionNonTxAware<R, A, LockException>
   {
      /**
       * @see org.exoplatform.services.transaction.ActionNonTxAware#getTransactionManager()
       */
      protected TransactionManager getTransactionManager()
      {
         return tm;
      }
   }

   /**
    * Remove all locks.
    */
   protected void removeAll()
   {
      List<LockData> locks = getLockList();
      for (LockData lockData : locks)
      {
         removeLock(lockData.getNodeIdentifier());
      }
   }

   /**
    * {@inheritDoc}
    */
   public void clean() throws BackupException
   {
      LOG.info("Start to clean lock Data");
      doClean();
   }

   /**
    * {@inheritDoc}
    */
   public void backup(File storageDir) throws BackupException
   {
      LOG.info("Start to backup lock data");
      ObjectOutputStream out = null;
      try
      {
         File contentFile = new File(storageDir, "CacheLocks" + DBBackup.CONTENT_FILE_SUFFIX);
         out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(contentFile)));
         
         List<LockData> locks = getLockList();

         out.writeInt(locks.size());
         for (LockData lockData : locks)
         {
            lockData.writeExternal(out);
         }
      }
      catch (FileNotFoundException e)
      {
         throw new BackupException(e);
      }
      catch (IOException e)
      {
         throw new BackupException(e);
      }
      finally
      {
         if (out != null)
         {
            try
            {
               out.flush();
               out.close();
            }
            catch (IOException e)
            {
               LOG.error("Can't close output stream", e);
            }
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public DataRestore getDataRestorer(DataRestoreContext context) throws BackupException
   {
      List<LockData> locks = new ArrayList<LockData>();

      ObjectInputStream in = null;
      try
      {
         File contentFile = new File((File)context.getObject(DataRestoreContext.STORAGE_DIR), 
                  "CacheLocks" + DBBackup.CONTENT_FILE_SUFFIX);

         // it is possible that backup was created on configuration without Backupable WorkspaceLockManager class
         if (!contentFile.exists())
         {
            return new DummyDataRestore();
         }

         in = new ObjectInputStream(new FileInputStream(contentFile));

         int count = in.readInt();
         for (int i = 0; i < count; i++)
         {
            LockData lockData = new LockData();
            lockData.readExternal(in);

            locks.add(lockData);
         }
      }
      catch (FileNotFoundException e)
      {
         throw new BackupException(e);
      }
      catch (IOException e)
      {
         throw new BackupException(e);
      }
      catch (ClassNotFoundException e)
      {
         throw new BackupException(e);
      }
      finally
      {
         if (in != null)
         {
            try
            {
               in.close();
            }
            catch (IOException e)
            {
               LOG.error("Can't close output stream", e);
            }
         }
      }

      return new CacheLocksRestore(locks);
   }

   /**
    * Cache restorer.
    */
   protected class CacheLocksRestore implements DataRestore
   {

      final private List<LockData> backupLocks = new ArrayList<LockData>();

      private List<LockData> actualLocks = new ArrayList<LockData>();

      CacheLocksRestore(final List<LockData> backupLocks)
      {
         this.backupLocks.addAll(backupLocks);
      }

      /**
       * {@inheritDoc}
       */
      public void clean() throws BackupException
      {
         LOG.info("Start to clean lock Data");
         actualLocks.addAll(getLockList());
         doClean();
      }

      /**
       * {@inheritDoc}
       */
      public void restore() throws BackupException
      {
         for (LockData lockData : backupLocks)
         {
            doPut(lockData);
         }
      }

      /**
       * {@inheritDoc}
       */
      public void commit() throws BackupException
      {
      }

      /**
       * {@inheritDoc}
       */
      public void rollback() throws BackupException
      {
         doClean();
         for (LockData lockData : actualLocks)
         {
            doPut(lockData);
         }
      }

      /**
       * {@inheritDoc}
       */
      public void close() throws BackupException
      {
         backupLocks.clear();
         actualLocks.clear();
      }
   }

   /**
    * Indicates if we are the only node in cluster.
    */
   protected abstract boolean isAloneInCluster();

   /**
    * Puts lock data directly into cache.
    * 
    * @param lockData
    *          the lock data to put
    * @return the old lock data previously added into cache         
    */
   protected abstract LockData doPut(LockData lockData);

   /**
    * Removes lock data directly from cache.
    * 
    * @param lockData
    *          the lock data to remove
    */
   protected abstract void doRemove(LockData lockData);

   /**
    * Clean cache directly.
    */
   protected abstract void doClean();

   /**
    * Returns {@link LockTableHandler} for lock table consistency checking.
    */
   public abstract LockTableHandler getLockTableHandler();

}
