//$Id: ManagedPersistenceContext.java 11990 2010-01-25 23:55:21Z manaRH $
package org.jboss.seam.persistence;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionEvent;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.FlushModeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Unwrap;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.contexts.Lifecycle;
import org.jboss.seam.core.Mutable;
import org.jboss.seam.core.Expressions.ValueExpression;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.transaction.Transaction;
import org.jboss.seam.transaction.UserTransaction;
import org.jboss.seam.util.Naming;

/**
 * A Seam component that manages a conversation-scoped extended
 * persistence context that can be shared by arbitrary other
 * components.
 * 
 * @author Gavin King
 */
@Scope(ScopeType.CONVERSATION)
@BypassInterceptors
@Install(false)
public class ManagedPersistenceContext 
   implements Serializable, HttpSessionActivationListener, Mutable, PersistenceContextManager, Synchronization
{
   private static final long serialVersionUID = -4972387440275848126L;
   private static final LogProvider log = Logging.getLogProvider(ManagedPersistenceContext.class);
   
   private transient EntityManager entityManager;
   private String persistenceUnitJndiName;
   private String componentName;
   private ValueExpression<EntityManagerFactory> entityManagerFactory;
   private List<Filter> filters = new ArrayList<Filter>(0);
   
   private transient boolean synchronizationRegistered;
   private transient boolean destroyed;
  
   public boolean clearDirty()
   {
      return true;
   }
   
   @Create
   public void create(Component component)
   {
      this.componentName = component.getName();
      if (persistenceUnitJndiName==null)
      {
         persistenceUnitJndiName = "java:/" + componentName;
      }
      
      PersistenceContexts.instance().touch(componentName);      
   }
   
   private void initEntityManager()
   {
      entityManager = getEntityManagerFactoryFromJndiOrValueBinding().createEntityManager();
      PersistenceProvider persistenceProvider = PersistenceProvider.instance();
      entityManager = persistenceProvider.proxyEntityManager(entityManager);
      setEntityManagerFlushMode( PersistenceContexts.instance().getFlushMode() );

      for (Filter f: filters)
      {
         if ( f.isFilterEnabled() )
         {
            persistenceProvider.enableFilter(f, entityManager);
         }
      }

      if ( log.isDebugEnabled() )
      {
         if (entityManagerFactory==null)
         {
            log.debug("created seam managed persistence context for persistence unit: "+ persistenceUnitJndiName);
         }
         else 
         {
            log.debug("created seam managed persistence context from EntityManagerFactory");
         }
      }
   }
   
   @Unwrap
   public EntityManager getEntityManager() throws NamingException, SystemException
   {
      if (entityManager==null) initEntityManager();
      
      if ( !synchronizationRegistered && !Lifecycle.isDestroying() )
      {
         joinTransaction();
      }
      
      return entityManager;
   }

   private void joinTransaction() throws SystemException
   {
      UserTransaction transaction = Transaction.instance();
      if ( transaction.isActive() )
      {
         transaction.enlist(entityManager);
         try
         {
            transaction.registerSynchronization(this);
            synchronizationRegistered = true;
         }
         catch (Exception e)
         {
            synchronizationRegistered = PersistenceProvider.instance().registerSynchronization(this, entityManager);
         }
      }
   }
   
   /**
    * If a transaction is active, fail the passivation. The field holding the
    * managed EntityManager is marked as transient so that it is not serialized
    * (it can't be). The transient keyword was choosen because we don't want to
    * forcefully close and nullify the EntityManager on every request because
    * then we have to keep hitting the database to load the entities back into
    * the persistence context. The only downside is that we cannot clean up
    * on the old node before the session hops, but it turns out not to matter.
    * 
    * Note that we must use the method on the
    * {@link HttpSessionActivationListener} interface rather than
    * <code>@PrePassivate</code> since interceptors are disabled on this component.
    */
   public void sessionWillPassivate(HttpSessionEvent event)
   {
      if (synchronizationRegistered)
      {
         throw new IllegalStateException("cannot passivate persistence context with active transaction");
      }
   }
   
   /**
    * Note that we must use the method on the {@link HttpSessionActivationListener}
    * interface rather than @PostActivate since interceptors are disabled
    * on this component.
    */
   public void sessionDidActivate(HttpSessionEvent event) {}
   
   @Destroy
   public void destroy()
   {
      destroyed = true;
      if ( !synchronizationRegistered )
      {
         //in requests that come through SeamPhaseListener,
         //there can be multiple transactions per request,
         //but they are all completed by the time contexts
         //are destroyed
         //so wait until the end of the request to close
         //the session
         //on the other hand, if we are still waiting for
         //the transaction to commit, leave it open
         close();
      }
      PersistenceContexts.instance().untouch(componentName);
   }

   public void afterCompletion(int status)
   {
      synchronizationRegistered = false;
      //if ( !Contexts.isConversationContextActive() )
      if (destroyed)
      {
         //in calls to MDBs and remote calls to SBs, the 
         //transaction doesn't commit until after contexts
         //are destroyed, so wait until the transaction
         //completes before closing the session
         //on the other hand, if we still have an active
         //conversation context, leave it open
         close();
      }
   }
   
   public void beforeCompletion() {}
   
   private void close()
   {
      if (Contexts.isEventContextActive()) 
      {

         boolean transactionActive = false;
         
         try
         {
            UserTransaction tx = Transaction.instance();
            try 
            {
               transactionActive = tx.isActive();
            }
            catch (SystemException se)
            {
               log.debug("could not get transaction status while destroying persistence context");
            }
         }
         catch (Exception e)
         {
            // WebSphere throws a javax.naming.ConfigurationException when Transaction.instance() is called during HTTP Session expiration 
            // and there is no JNDI lookup possible. See details there: JBSEAM-4332
            log.warn("could not get transaction while destroying persistence context. (called during session expiration ?)");
         }
         
         if ( transactionActive )
         {
            throw new IllegalStateException("attempting to destroy the persistence context while an active transaction exists (try installing <transaction:ejb-transaction/>)");
         }
      }
      
      if ( log.isDebugEnabled() )
      {
         log.debug("destroying seam managed persistence context for persistence unit: " + persistenceUnitJndiName);
      }
      
      if (entityManager!=null && entityManager.isOpen())
      {
         entityManager.close();
      }
   }
   
   public EntityManagerFactory getEntityManagerFactoryFromJndiOrValueBinding()
   {
      EntityManagerFactory result = null;
      //first try to find it via the value binding
      if (entityManagerFactory!=null)
      {
         result = entityManagerFactory.getValue();
      }
      //if its not there, try JNDI
      if (result==null)
      {
         try
         {
            result = (EntityManagerFactory) Naming.getInitialContext().lookup(persistenceUnitJndiName);
         }
         catch (NamingException ne)
         {
            throw new IllegalArgumentException("EntityManagerFactory not found in JNDI : " + persistenceUnitJndiName, ne);
         }
      }
      return result;
   }
   
   /**
    * A value binding expression that returns an EntityManagerFactory,
    * for use of JPA outside of Java EE 5 / Embeddable EJB3.
    */
   public ValueExpression<EntityManagerFactory> getEntityManagerFactory()
   {
      return entityManagerFactory;
   }
   
   public void setEntityManagerFactory(ValueExpression<EntityManagerFactory> entityManagerFactory)
   {
      this.entityManagerFactory = entityManagerFactory;
   }
   
   /**
    * The JNDI name of the EntityManagerFactory, for 
    * use of JPA in Java EE 5 / Embeddable EJB3.
    */
   public String getPersistenceUnitJndiName()
   {
      return persistenceUnitJndiName;
   }
   
   public void setPersistenceUnitJndiName(String persistenceUnitName)
   {
      this.persistenceUnitJndiName = persistenceUnitName;
   }
   
   public String getComponentName() 
   {
      return componentName;
   }
   
   /**
    * Hibernate filters to enable automatically
    */
   public List<Filter> getFilters()
   {
      return filters;
   }
   
   public void setFilters(List<Filter> filters)
   {
      this.filters = filters;
   }
   
   public void changeFlushMode(FlushModeType flushMode)
   {
      if (entityManager!=null && entityManager.isOpen())
      {
         setEntityManagerFlushMode(flushMode);
      }
   }
   
   protected void setEntityManagerFlushMode(FlushModeType flushMode)
   {
      switch (flushMode)
      {
         case AUTO:
            entityManager.setFlushMode(javax.persistence.FlushModeType.AUTO);
            break;
         case COMMIT:
            entityManager.setFlushMode(javax.persistence.FlushModeType.COMMIT);
            break;
         case MANUAL:
            PersistenceProvider.instance().setFlushModeManual(entityManager);
            break;
      }
   }
   
   @Override
   public String toString()
   {
      return "ManagedPersistenceContext(" + persistenceUnitJndiName + ")";
   }
}
