/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.impl.core;

import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.services.jcr.config.RepositoryEntry;
import org.exoplatform.services.jcr.impl.proccess.WorkerThread;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a>
 * @version $Id: SessionRegistry.java 12049 2008-03-18 12:22:03Z gazarenkov $
 */
@Managed
@NameTemplate(@Property(key = "service", value = "SessionRegistry"))
public final class SessionRegistry implements Startable
{
   private final Map<String, SessionImpl> sessionsMap;

   // 1 min
   public final static int DEFAULT_CLEANER_TIMEOUT = 60 * 1000;

   // 10 mins
   public final static int DEFAULT_SESSION_TIMEOUT = 10 * 60 * 1000;

   protected static Log log = ExoLogger.getLogger("exo.jcr.component.core.SessionRegistry");

   private volatile SessionCleaner sessionCleaner;

   private String repositoryId;

   protected volatile long timeOut;

   @Managed
   @ManagedDescription("How many sessions are currently active")
   public int getSize()
   {
      return sessionsMap.size();
   }

   @Managed
   @ManagedDescription("The session time out")
   public long getTimeOut()
   {
      return timeOut;
   }

   @Managed
   @ManagedDescription("Set the session time out in seconds")
   public void setTimeOut(long timeout)
   {
      if (this.timeOut == timeout)
      {
         return;
      }

      // disable the cleaner
      this.sessionCleaner.halt();
      this.sessionCleaner = null;

      this.timeOut = timeout <= 0 ? 0 : timeout * 1000;
      if (timeOut == 0 && sessionCleaner != null)
      {
         // We set a time out to 0, no need to create new cleaner
         if (log.isDebugEnabled())
         {
            log.debug("Stop the previous session cleaner");
         }
      }
      else if (timeOut > 0 && sessionCleaner == null)
      {
         // We set a time out greater than 0, so we create new cleaner
         this.sessionCleaner = new SessionCleaner(repositoryId, DEFAULT_CLEANER_TIMEOUT, timeOut);      
         if (log.isDebugEnabled())
         {
            log.debug("Start a new session cleaner");            
         }
      }
   }

   @Managed
   @ManagedDescription("Perform a cleanup of timed out sessions")
   public void runCleanup()
   {
      if (sessionCleaner != null)
      {
         try
         {
            sessionCleaner.callPeriodically();
         }
         catch (Exception e)
         {
            log.warn("Could not execute the cleanup command", e);
         }
      }
   }

   public SessionRegistry(RepositoryEntry entry)
   {
      this(null, entry);
   }

   public SessionRegistry(ExoContainerContext ctx, RepositoryEntry entry)
   {
      sessionsMap = new ConcurrentHashMap<String, SessionImpl>();
      if (entry != null)
      {
         this.timeOut = entry.getSessionTimeOut() > 0 ? entry.getSessionTimeOut() : DEFAULT_SESSION_TIMEOUT;
      }
      this.repositoryId = ctx != null ? ctx.getName() : (entry == null ? null : entry.getName());
   }

   public void registerSession(SessionImpl session)
   {
      sessionsMap.put(session.getId(), session);
   }

   public void unregisterSession(String sessionId)
   {
      sessionsMap.remove(sessionId);
   }

   public SessionImpl getSession(String sessionId)
   {
      return sessionId == null ? null : sessionsMap.get(sessionId);
   }

   public boolean isInUse(String workspaceName)
   {
      if (workspaceName == null)
      {
         if (log.isDebugEnabled())
            log.debug("Session in use " + sessionsMap.size());
         return sessionsMap.size() > 0;
      }
      for (SessionImpl session : sessionsMap.values())
      {
         if (session.getWorkspace().getName().equals(workspaceName))
         {
            if (log.isDebugEnabled())
               log.debug("Session for workspace " + workspaceName + " in use." + " Session id:" + session.getId()
                  + " user: " + session.getUserID());
            return true;
         }
      }
      return false;
   }

   public void start()
   {
      sessionsMap.clear();

      if (timeOut > 0)
         sessionCleaner = new SessionCleaner(repositoryId, DEFAULT_CLEANER_TIMEOUT, timeOut);
   }

   public void stop()
   {
      if (timeOut > 0 && sessionCleaner != null)
         sessionCleaner.halt();
      // Close all the current sessions to prevent memory leaks
      for (SessionImpl s : sessionsMap.values())
      {
         try
         {
            s.logout();
         }
         catch (Exception e)
         {
            if (log.isDebugEnabled())
            {
               log.debug("Could not close the session", e);
            }
         }
      }
      sessionsMap.clear();
   }

   /**
    * closeSessions will be closed the all sessions on specific workspace.
    *
    * @param workspaceName
    *          the workspace name.
    * @return int
    *           how many sessions was closed.
    */
   public int closeSessions(String workspaceName)
   {
      int closedSessions = 0;

      for (SessionImpl session : sessionsMap.values())
      {
         if (session.getWorkspace().getName().equals(workspaceName))
         {
            session.logout();
            closedSessions++;
         }
      }

      return closedSessions;
   }

   private class SessionCleaner extends WorkerThread
   {

      private final long sessionTimeOut;

      public SessionCleaner(String id, long workTime, long sessionTimeOut)
      {
         super(workTime);
         this.sessionTimeOut = sessionTimeOut;
         setName("Session Cleaner " + (id == null ? getId() : id));
         setPriority(Thread.MIN_PRIORITY);
         setDaemon(true);
         start();
         if (log.isDebugEnabled())
            log.debug("SessionCleaner instantiated name= " + getName() + " workTime= " + workTime + " sessionTimeOut="
               + sessionTimeOut);
      }

      @Override
      protected void callPeriodically() throws Exception
      {
         for (SessionImpl session : sessionsMap.values())
         {
            if (session.getLastAccessTime() + getTimeout(session) < System.currentTimeMillis())
            {
               session.expire();
            }
         }
      }
      
      /**
       * Checks if the session has a local timeout if so it will use it otherwise it will use the
       * global timeout
       */
      private long getTimeout(SessionImpl session)
      {
         long localSessionTimeout = session.getTimeout();
         return localSessionTimeout == 0 ? sessionTimeOut : localSessionTimeout;
      }
   }
}
