/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.hibernate.session;

import net.sf.hibernate.Session;
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.SessionFactory;

import org.jboss.logging.Logger;
import org.jboss.tm.TransactionLocal;
import org.jboss.tm.TxManager;
import org.jboss.util.NestedRuntimeException;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.TransactionManager;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.Status;
import java.util.HashMap;
import java.util.Map;

/**
 * Maintains and exposes, for app usage, the current context bound Hibernate Session.
 * Application code need only deal with the {@link #getSession(java.lang.String)}
 * as the means to retreive the {@link net.sf.hibernate.Session} associated with
 * the current context.
 *
 * @author <a href="mailto:steve@hibernate.org">Steve Ebersole</a>
 * @version $Revision: 1.2.2.3 $
 */
public class HibernateContext
{
   private static final Logger log = Logger.getLogger(HibernateContext.class);

   private static final TransactionLocal sessions = new TransactionLocal()
   {
      protected Object initialValue()
      {
         return new HashMap();
      }
   };

   /**
    * Retreives an "unmanaged" session against the same underlying jdbc connnection as the session
    * currently bound to the current context for the given JNDI name.  This is simply a convenience
    * method for SessionFactory.openSession({@link #getSession}.connection()).  Unmanaged here means that
    * the returned session is not controlled by the code managing the actually bound session; callers
    * are required to cleanup these sessions manually using {@link #releaseUnmanagedSession}.
    *
    * @param name The "name" of the {@link net.sf.hibernate.SessionFactory}
    *       for which an unmanaged session is requested.
    * @return An unmanaged session.
    * @throws HibernateException If an error occurs opening the new Session.
    * @throws IllegalStateException If unable to locate a managed Session for the current context.
    */
   public static Session getUnmanagedSession(String name) throws HibernateException, IllegalStateException
   {
      final Session managedSession = lookupSession(name);
      if (managedSession == null)
      {
         throw new IllegalStateException("No managed session found for current context");
      }
      return managedSession.getSessionFactory().openSession( managedSession.connection() );
   }

   /**
    * Method to release a previously obtained unmanaged session.
    *
    * @param unmanagedSession The unmanaged Session to release.
    * @throws HibernateException If an error occurs releasing the unmanaged Session.
    */
   public static void releaseUnmanagedSession(Session unmanagedSession) throws HibernateException
   {
      unmanagedSession.close();
   }

   /**
    * Retreives the session currently bound to the current context.
    *
    * @param name The "name" of the {@link net.sf.hibernate.SessionFactory}
    *       for which a session is requested.
    * @return The current session.
    */
   public static Session getSession(String name)
   {
      try
      {
         // Determine whether a session is already bound to the current transaction.
         // If so, return that session; otherwise generate a new session, bind, and
         // return it
         Session currentSession = lookupSession(name);
         if (currentSession == null)
         {
            log.trace("No session bound; generating");
            currentSession = generateSession(name);
            prepareSession(name, currentSession);
            bind(name, currentSession);
         }
         return currentSession;
      }
      catch(HibernateException e)
      {
         throw new NestedRuntimeException("Unable to retreive Session", e);
      }
   }

   /**
    * Determines whether a session is already bound to the current context
    * for the given name.
    *
    * @param name The name to be checked within the current context.
    * @return True if a session is already bound; false otherwise.
    */
   public static boolean hasBind(String name)
   {
      return ( (Map) sessions.get() ).containsKey(name);
   }

   /**
    * Binds a session to the current context.  Can be used along with {@link #unbind}
    * (and optionally {@link #prepareSession}) for applications to manage lifecycle of
    * Hibernate Sessions themselves.
    *
    * Note that these methods still require that a JTA transaction be ongoing prior to
    * any attempt to bind the session.
    *
    * @param name The name under which to bind the session.
    * @param currentSession The session to bind
    * @throws HibernateException If a session already bound under the given name
    * within the current context.
    */
   public static void bind(String name, Session currentSession) throws HibernateException
   {
      if ( hasBind(name) )
      {
         throw new HibernateException("Session already bound to [" + name + "] for current transaction");
      }
      ( (Map) sessions.get() ).put(name, currentSession);
   }

   /**
    * Unbinds a session from the current context.
    *
    * @see #bind for more details.
    * @param name The name from which to unbind.
    * @return The session previously bound under the given name; conceiveably null.
    */
   public static Session unbind(String name)
   {
      return (Session) ( (Map) sessions.get() ).remove(name);
   }

   /**
    * Prepare a session for binding.  Mainly, this entails creation and registration
    * of a transaction synch with the current transaction for cleanup purposes.
    *
    * @param session The session to prepare.
    * @param name The name under which the session is to be bound
    * @throws HibernateException If there was a problem registering the synch.
    */
   public static void prepareSession(String name, Session session) throws HibernateException
   {
      InitialContext context = null;
      try
      {
         context = new InitialContext();
         TransactionManager tm = (TransactionManager) context.lookup("java:/TransactionManager");
         tm.getTransaction().registerSynchronization( new TransactionSynch(name, session) );
      }
      catch(NamingException e)
      {
         throw new HibernateException("Unable to locate TransactionManager", e);
      }
      catch(IllegalStateException e)
      {
         throw new HibernateException("Unable to register transaction synch", e);
      }
      catch(RollbackException e)
      {
         throw new HibernateException("Unable to register transaction synch", e);
      }
      catch(SystemException e)
      {
         throw new HibernateException("Unable to locate Transaction", e);
      }
      finally
      {
         release(context);
      }
   }

   private static Session lookupSession(String name)
   {
      return (Session) ( (Map) sessions.get() ).get(name);
   }

   private static Session generateSession(String name) throws HibernateException
   {
      // Make sure there is an ongoing transaction prior to generating
      // a session in order to provide a usefull error message...
      checkTransactionStatus();

      SessionFactory factory = locateSessionFactory(name);
      return factory.openSession();
   }

   private static void checkTransactionStatus() throws HibernateException
   {
// protected access in 3.2
//      Transaction transaction = sessions.getTransaction();

      try
      {
         Transaction transaction = TxManager.getInstance().getTransaction();
         if (transaction != null)
         {
            if (transaction.getStatus() == Status.STATUS_ACTIVE)
            {
               // all ok
               return;
            }
         }
      }
      catch(SystemException e)
      {
         // do nothing...
      }

      // otherwise, throw a meaningful exception
      throw new HibernateException("Session cannot be generated outside of transaction scope");
   }

   private static SessionFactory locateSessionFactory(String name) throws HibernateException
   {
      InitialContext context = null;
      try
      {
         context = new InitialContext();
         final SessionFactory factory = (SessionFactory) context.lookup(name);
         return factory;
      }
      catch(NamingException e)
      {
         throw new HibernateException("Unable to locate SessionFactory in JNDI under name [" + name + "]", e);
      }
      finally
      {
         release(context);
      }
   }

   private static void release(InitialContext ctx)
   {
      if (ctx != null)
      {
         try
         {
            ctx.close();
         }
         catch(Throwable t)
         {
            log.info("Unable to release context", t);
         }
      }
   }
}
