/*
 * 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.services.jcr.core.ExtendedSession;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.impl.core.NodeImpl;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.SessionImpl;
import org.exoplatform.services.jcr.impl.core.lock.AbstractSessionLockManager;
import org.exoplatform.services.jcr.impl.core.lock.LockImpl;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;

/**
 * Created by The eXo Platform SAS.
 * 
 * <br>Date:
 *
 * @author <a href="karpenko.sergiy@gmail.com">Karpenko Sergiy</a> 
 * @version $Id: CacheableSessionLockManager.java 2137 2010-03-25 15:31:56Z sergiykarpenko $
 */
public class CacheableSessionLockManager extends AbstractSessionLockManager
{
   /**
    * Logger
    */
   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.CacheableSessionLockManager");

   /**
    * Session identifier.
    */
   private final String sessionID;

   /**
    * Lock tokens held by this session.
    * [token name, tokens hash]
    */
   private final Map<String, String> tokens;

   /**
    * Map of nodes locked in this session. Need to remove session scoped lock on session close.
    * [node identifier, lock data]
    */
   private final Map<String, LockData> lockedNodes;

   /**
    * Set of pending locked nodes identifiers.
    */
   private final Set<String> pendingLocks;

   /**
    * Workspace lock manager
    */
   private final CacheableLockManager lockManager;

   /**
    * Constructor.
    * 
    * @param sessionID - session identifier
    * @param lockManager - workspace lock manager
    */
   public CacheableSessionLockManager(String sessionID, CacheableLockManager lockManager,
      SessionDataManager transientManager)
   {
      super(transientManager);
      this.sessionID = sessionID;
      this.tokens = new HashMap<String, String>();
      this.lockedNodes = new HashMap<String, LockData>();
      this.pendingLocks = new HashSet<String>();
      this.lockManager = lockManager;
   }

   /**
    * {@inheritDoc}
    */
   public Lock addLock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timeOut) throws LockException,
      RepositoryException
   {

      String lockToken = IdGenerator.generate();

      NodeData data = (NodeData)node.getData();

      LockData lData = lockManager.getExactNodeOrCloseParentLock(data);
      if (lData != null)
      {
         if (lData.getNodeIdentifier().equals(node.getIdentifier()))
         {
            throw new LockException("Node already locked: " + data.getQPath());
         }
         else if (lData.isDeep())
         {
            throw new LockException("Parent node has deep lock.");
         }
      }

      if (isDeep && lockManager.getClosedChild(data) != null)
      {
         throw new LockException("Some child node is locked.");
      }

      String lockTokenHash = lockManager.getLockTokenHash(lockToken);

      lData =
         new LockData(node.getIdentifier(), lockTokenHash, isDeep, isSessionScoped, node.getSession().getUserID(),
            timeOut > 0 ? timeOut : lockManager.getDefaultLockTimeOut());

      lockedNodes.put(node.getInternalIdentifier(), lData);
      pendingLocks.add(node.getInternalIdentifier());
      tokens.put(lockToken, lData.getTokenHash());

      LockImpl lock = new CacheLockImpl(node.getSession(), lData, this);

      return lock;
   }

   /**
    * {@inheritDoc}
    */
   public void addLockToken(String lt)
   {
      tokens.put(lt, lockManager.getLockTokenHash(lt));
   }

   /**
    * {@inheritDoc}
    */
   public LockImpl getLock(NodeImpl node) throws LockException, RepositoryException
   {
      LockData lData = lockManager.getExactNodeOrCloseParentLock((NodeData)node.getData());

      if (lData == null || (!node.getInternalIdentifier().equals(lData.getNodeIdentifier()) && !lData.isDeep()))
      {
         throw new LockException("Node not locked: " + node.getData().getQPath());
      }
      return new CacheLockImpl(node.getSession(), lData, this);
   }

   /**
    * {@inheritDoc}
    */
   public String[] getLockTokens()
   {
      String[] arr = new String[tokens.size()];
      tokens.keySet().toArray(arr);
      return arr;
   }

   /**
    * {@inheritDoc}
    */
   public boolean holdsLock(NodeData node) throws RepositoryException
   {
      return lockManager.lockExist(node.getIdentifier());//.getExactNodeLock(node) != null;
   }

   /**
    * {@inheritDoc}
    */
   protected boolean isLockedPersisted(NodeData node) throws LockException
   {
      LockData lData = null;
      try
      {
         lData = lockManager.getExactNodeOrCloseParentLock(node);
      }
      catch (RepositoryException e)
      {
         throw new LockException(e.getMessage(), e);
      }

      if (lData == null || (!node.getIdentifier().equals(lData.getNodeIdentifier()) && !lData.isDeep()))
      {
         return false;
      }
      return true;
   }

   /**
    * {@inheritDoc}
    */
   protected boolean isPersistedLockHolder(NodeData node) throws RepositoryException
   {
      LockData lData = lockManager.getExactNodeOrCloseParentLock(node);
      return lData != null && isLockHolder(lData);
   }

   /**
    * {@inheritDoc}
    */
   public void onCloseSession(ExtendedSession session)
   {
      SessionImpl sessionImpl = (SessionImpl)session;

      int length = lockedNodes.size();
      if (length > 0)
      {
         String[] nodeIds = new String[length];
         lockedNodes.keySet().toArray(nodeIds);

         for (int i = 0; i < length; i++)
         {
            String nodeId = nodeIds[i];
            LockData lock = lockedNodes.remove(nodeId);

            if (lock.isSessionScoped() && !pendingLocks.contains(nodeId))
            {
               try
               {
                  NodeImpl node =
                     ((NodeImpl)sessionImpl.getTransientNodesManager()
                        .getItemByIdentifier(lock.getNodeIdentifier(), false));

                  if (node != null)
                  {
                     node.unlock();
                  }

               }
               catch (UnsupportedRepositoryOperationException e)
               {
                  LOG.error(e.getLocalizedMessage());
               }
               catch (LockException e)
               {
                  LOG.error(e.getLocalizedMessage());
               }
               catch (AccessDeniedException e)
               {
                  LOG.error(e.getLocalizedMessage());
               }
               catch (RepositoryException e)
               {
                  LOG.error(e.getLocalizedMessage());
               }
            }
         }
         lockedNodes.clear();
      }

      pendingLocks.clear();
      tokens.clear();

      lockManager.closeSessionLockManager(sessionID);
   }

   /**
    * {@inheritDoc}
    */
   public void removeLockToken(String lt)
   {
      tokens.remove(lt);
   }

   /**
    * Checks if session has token to this lock data or session is System.
    * 
    * @param lockData
    * @return
    */
   private boolean isLockHolder(LockData lockData)
   {
      return tokens.containsValue(lockData.getTokenHash());
   }

   /**
    * Is session contains pending lock for node by nodeId.
    * @param nodeId - node ID string
    * @return boolean
    */
   public boolean containsPendingLock(String nodeId)
   {
      return pendingLocks.contains(nodeId);
   }

   /**
    * Returns real token, if session has it.
    * 
    * @param tokenHash - token hash string
    * @return lock token string
    */
   protected String getLockToken(String tokenHash)
   {
      for (String token : tokens.keySet())
      {
         if (tokens.get(token).equals(tokenHash))
         {
            return token;
         }
      }
      return null;
   }

   /**
    * Return pending lock.
    * 
    * @param nodeId - ID of locked node
    * @return pending lock or null
    */
   public LockData getPendingLock(String nodeId)
   {
      if (pendingLocks.contains(nodeId))
      {
         return lockedNodes.get(nodeId);
      }
      else
      {
         return null;
      }
   }

   /**
    * Check is lock alive. That means lock must exist in LockManager storage (cache or map, etc). 
    * 
    * @param nodeIdentifier - locked node id
    * @return
    */
   protected boolean isLockLive(String nodeIdentifier) throws LockException
   {

      if (lockManager.isLockLive(nodeIdentifier))
      {
         return true;
      }
      else
      {
         return pendingLocks.contains(nodeIdentifier);
      }
   }

   public void notifyLockPersisted(String nodeIdentifier)
   {
      pendingLocks.remove(nodeIdentifier);
   }

   /**
    * Notify SessionLockManager that node is unlocked.
    * 
    * @param nodeIdentifier - unlocked node identifier
    */
   public void notifyLockRemoved(String nodeIdentifier)
   {
      lockedNodes.remove(nodeIdentifier);
   }

   /**
    * Refresh lockData. 
    * 
    * @param newLockData
    * @throws LockException
    */
   protected void refresh(LockData newLockData) throws LockException
   {
      lockManager.refreshLockData(newLockData);
   }

   /**
    * {@inheritDoc}
    */
   protected boolean checkPersistedLocks(NodeData node) throws LockException
   {
      LockData lData = null;
      try
      {
         lData = lockManager.getExactNodeOrCloseParentLock(node);
      }
      catch (RepositoryException e)
      {
         throw new LockException(e.getMessage(), e);
      }

      if (lData == null || (!node.getIdentifier().equals(lData.getNodeIdentifier()) && !lData.isDeep()))
      {
         return true;
      }

      // lock exist, so lets check is current session is LockHolder
      return isLockHolder(lData);
   }

}
