/*
* JBoss, the OpenSource J2EE webOS
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package org.jboss.ejb.plugins;

import org.jboss.ejb.BeanLock;
import org.jboss.ejb.Container;
import org.jboss.ejb.EntityContainer;
import org.jboss.ejb.EntityEnterpriseContext;
import org.jboss.ejb.InstanceCache;
import org.jboss.ejb.AllowedOperationsAssociation;
import org.jboss.ejb.InstancePool;
import org.jboss.invocation.Invocation;
import org.jboss.util.NestedRuntimeException;

import javax.ejb.NoSuchObjectLocalException;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.transaction.Transaction;
import java.lang.reflect.Method;
import java.rmi.NoSuchObjectException;
import java.rmi.RemoteException;

/**
 * The instance interceptors role is to acquire a context representing the
 * target object from the cache.
 *
 * <p>This particular container interceptor implements pessimistic locking on
 * the transaction that is associated with the retrieved instance.  If there is
 * a transaction associated with the target component and it is different from
 * the transaction associated with the Invocation coming in then the policy is
 * to wait for transactional commit.
 *
 * <p>We also implement serialization of calls in here (this is a spec
 * requirement). This is a fine grained notify, notifyAll mechanism. We notify
 * on ctx serialization locks and notifyAll on global transactional locks.
 *
 * <p><b>WARNING: critical code</b>, get approval from senior developers before
 * changing.
 * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a>
 * @author <a href="mailto:Scott.Stark@jboss.org">Scott Stark</a>
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @author <a href="mailto:mkgarnek@hotmail.com">Jamie Burns</a>
 * @version $Revision: 1.75.4.1 $
 */
public class EntityInstanceInterceptor
   extends AbstractInterceptor
{
   // Constants -----------------------------------------------------
    
   // Attributes ----------------------------------------------------
    
   protected EntityContainer container;

   // Static --------------------------------------------------------

   /** A reference to {@link javax.ejb.TimedObject#ejbTimeout}. */
   protected static final Method ejbTimeout;

   static
   {
      try
      {
         ejbTimeout = TimedObject.class.getMethod("ejbTimeout", new Class[]{Timer.class});
      }
      catch (Exception e)
      {
         throw new ExceptionInInitializerError(e);
      }
   }

   // Constructors --------------------------------------------------
    
   // Public --------------------------------------------------------
    
   public void setContainer(Container container)
   {
      this.container = (EntityContainer) container;
   }

   public Container getContainer()
   {
      return container;
   }

   // Interceptor implementation --------------------------------------

   public Object invokeHome(Invocation mi)
      throws Exception
   {
      // Get context
      EntityContainer container = (EntityContainer) getContainer();
      EntityEnterpriseContext ctx = (EntityEnterpriseContext) container.getInstancePool().get();
      InstancePool pool = container.getInstancePool();

      // Pass it to the method invocation
      mi.setEnterpriseContext(ctx);
   
      // Give it the transaction
      ctx.setTransaction(mi.getTransaction());
   
      // Set the current security information
      ctx.setPrincipal(mi.getPrincipal());

      AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_HOME);
      
      // Invoke through interceptors
      
      Object obj = null;
      Exception exception = null;

      try
      {
         obj = getNext().invokeHome(mi);
          
         // Is the context now with an identity? in which case we need to insert
         if (ctx.getId() != null)
         {
            BeanLock lock = container.getLockManager().getLock(ctx.getCacheKey());
            lock.sync(); // lock all access to BeanLock
            try
            {
               // Check there isn't a context already in the cache
               // e.g. commit-option B where the entity was
               // created then removed externally
               InstanceCache cache = container.getInstanceCache();
               cache.remove(ctx.getCacheKey());
       
               // marcf: possible race on creation and usage
               // insert instance in cache,
               cache.insert(ctx);
            }
            finally
            {
               lock.releaseSync();
               container.getLockManager().removeLockRef(ctx.getCacheKey());
            }
             
            // we are all done             
            return obj;
         }
      }
      catch (Exception e)
      {
         exception = e;
      }
      finally
      {
         AllowedOperationsAssociation.popInMethodFlag();
      }

      ctx.setTransaction(null);
      // EntityCreateInterceptor will access ctx if it is not null and call postCreate
      mi.setEnterpriseContext(null);
      
      // if we get to here with a null exception then our invocation is
      // just a home invocation. Return our instance to the instance pool   
      if (exception == null)
      {
         container.getInstancePool().free(ctx);
         return obj;
      }
      
      if (exception instanceof RuntimeException)
      {
         // if we get to here with a RuntimeException, we have a system exception.
         // EJB 2.1 section 18.3.1 says we need to discard our instance.
         pool.discard(ctx);
      }
      else
      {
         // if we get to here with an Exception, we have an application exception.
         // EJB 2.1 section 18.3.1 says we can keep the instance. We need to return 
         // our instance to the instance pool so we dont get a memory leak.  
         pool.free(ctx);         
      }
      
      throw exception;
   }


   public Object invoke(Invocation mi)
      throws Exception
   {
      boolean trace = log.isTraceEnabled();

      // The key
      Object key = mi.getId();

      // The context
      EntityEnterpriseContext ctx;
      try
      {
         ctx = (EntityEnterpriseContext) container.getInstanceCache().get(key);
      }
      catch (NoSuchObjectException e)
      {
         if (mi.isLocal())
            throw new NoSuchObjectLocalException(e.getMessage());
         else
            throw e;
      }

      if (trace) log.trace("Begin invoke, key=" + key);

      // Associate transaction, in the new design the lock already has the transaction from the
      // previous interceptor

      // Don't set the transction if a read-only method.  With a read-only method, the ctx can be shared
      // between multiple transactions.
      Transaction tx = mi.getTransaction();
      if (!container.isReadOnly())
      {
         Method method = mi.getMethod();
         if (method == null ||
            !container.getBeanMetaData().isMethodReadOnly(method.getName()))
         {
            ctx.setTransaction(tx);
         }
      }

      // Set the current security information
      ctx.setPrincipal(mi.getPrincipal());
      // Set the JACC EnterpriseBean PolicyContextHandler data
      EnterpriseBeanPolicyContextHandler.setEnterpriseBean(ctx.getInstance());

      // Set context on the method invocation
      mi.setEnterpriseContext(ctx);

      if (ejbTimeout.equals(mi.getMethod()))
         AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_TIMEOUT);
      else
         AllowedOperationsAssociation.pushInMethodFlag(IN_BUSINESS_METHOD);

      Throwable exceptionThrown = null;
      boolean discardContext = false;
      try
      {
         Object obj = getNext().invoke(mi);
         return obj;
      }
      catch (RemoteException e)
      {
         exceptionThrown = e;
         discardContext = true;
         throw e;
      }
      catch (RuntimeException e)
      {
         exceptionThrown = e;
         discardContext = true;
         throw e;
      }
      catch (Error e)
      {
         exceptionThrown = e;
         discardContext = true;
         throw e;
      }
      catch (Exception e)
      {
         exceptionThrown = e;
         throw e;
      }
      catch (Throwable e)
      {
         exceptionThrown = e;
         discardContext = true;
         throw new NestedRuntimeException(e);
      }
      finally
      {
         AllowedOperationsAssociation.popInMethodFlag();

         // Make sure we clear the transaction on an error before synchronization.
         // But avoid a race with a transaction rollback on a synchronization
         // that may have moved the context onto a different transaction
         if (exceptionThrown != null && tx != null)
         {
            Transaction ctxTx = ctx.getTransaction();
            if (tx.equals(ctxTx) && ctx.hasTxSynchronization() == false)
               ctx.setTransaction(null);
         }

         // If an exception has been thrown,
         if (exceptionThrown != null &&
            // if tx, the ctx has been registered in an InstanceSynchronization.
            // that will remove the context, so we shouldn't.
            // if no synchronization then we need to do it by hand
            // But not for application exceptions
            !ctx.hasTxSynchronization() && discardContext)
         {
            // Discard instance
            // EJB 1.1 spec 12.3.1
            container.getInstanceCache().remove(key);

            if (trace) log.trace("Ending invoke, exceptionThrown, ctx=" + ctx, exceptionThrown);
         }
         else if (ctx.getId() == null)
         {
            // The key from the Invocation still identifies the right cachekey
            container.getInstanceCache().remove(key);

            if (trace) log.trace("Ending invoke, cache removal, ctx=" + ctx);
            // no more pool return
         }

         if (trace) log.trace("End invoke, key=" + key + ", ctx=" + ctx);

      } // end invoke
   }
}