/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.ejb;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Set;
import java.rmi.RemoteException;
import java.security.Principal;

import javax.ejb.EJBObject;
import javax.ejb.EJBLocalObject;
import javax.ejb.RemoveException;
import javax.ejb.EJBException;
import javax.ejb.Handle;
import javax.management.ObjectName;

import org.jboss.ejb.plugins.StatefulSessionInstanceCache;
import org.jboss.invocation.Invocation;
import org.jboss.invocation.InvocationType;
import org.jboss.security.SimplePrincipal;
import org.jboss.util.UnreachableStatementException;

/**
 * The container for <em>stateful</em> session beans.
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard Oberg</a>
 * @author <a href="mailto:docodan@mvcsoft.com">Daniel OConnor</a>
 * @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:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:Christoph.Jung@infor.de">Christoph G. Jung</a>
 * @version <tt>$Revision: 98926 $</tt>
 * @jmx:mbean extends="org.jboss.ejb.ContainerMBean"
 */

public class StatefulSessionContainer
   extends SessionContainer
   implements EJBProxyFactoryContainer, InstancePoolContainer, StatefulSessionContainerMBean
{
   /**
    * This is the persistence manager for this container
    */
   protected StatefulSessionPersistenceManager persistenceManager;

   /**
    * The instance cache.
    */
   protected InstanceCache instanceCache;
   protected Method getEJBObject;
   protected Method ejbObjectRemove;
   protected Method ejbLocalObjectRemove;

   public void setInstanceCache(InstanceCache ic)
   {
      this.instanceCache = ic;
   }

   public InstanceCache getInstanceCache()
   {
      return instanceCache;
   }

   public long getPassivatedCount()
   {
      if (! (instanceCache instanceof StatefulSessionInstanceCache))
         throw new UnsupportedOperationException();

      return ((StatefulSessionInstanceCache)instanceCache).getPassivatedCount();
   }

   public StatefulSessionPersistenceManager getPersistenceManager()
   {
      return persistenceManager;
   }

   public void setPersistenceManager(StatefulSessionPersistenceManager pm)
   {
      persistenceManager = pm;
   }

   /**
    * Override getMethodPermissions to work around the fact that stateful
    * session handles obtain their ejb objects by doing an invocation on the
    * container as a home method invocation using the Handle.getEJBObject
    * method.
    * @param m
    * @param iface
    * @return
    */
   public Set<Principal> getMethodPermissions(Method m, InvocationType iface)
   {
      if (m.equals(getEJBObject) == false)
         return super.getMethodPermissions(m, iface);

      Class[] sig = {};
      Set<String> permissions = getBeanMetaData().getMethodPermissions("create",
         sig, iface);
      //Convert this into Set of Principals
      Set<Principal> principalSet = new HashSet<Principal>();
      for(String perm: permissions)
      {
         principalSet.add(new SimplePrincipal(perm));
      }
      return principalSet;
   }

   // Container implementation --------------------------------------
   protected void createService() throws Exception
   {
      super.createService();
      // Get the Handle.getEJBObject method for permission checks
      try
      {
         getEJBObject = Handle.class.getMethod("getEJBObject", new Class[0]);
      }
      catch (Exception e)
      {
         log.warn("Failed to grant access to the Handle.getEJBObject method");
      }

      ejbObjectRemove = EJBObject.class.getMethod("remove", null);
      ejbLocalObjectRemove = EJBLocalObject.class.getMethod("remove", null);
   }

   /**
    * creates and registers the instance cache
    */
   protected void createInstanceCache() throws Exception
   {
      // Try to register the instance cache as an MBean
      try
      {
         ObjectName containerName = super.getJmxName();
         Hashtable props = containerName.getKeyPropertyList();
         props.put("plugin", "cache");
         ObjectName cacheName = new ObjectName(containerName.getDomain(), props);
         server.registerMBean(instanceCache, cacheName);
      }
      catch (Throwable t)
      {
         log.debug("Failed to register cache as mbean", t);
      }
      // Init instance cache
      instanceCache.setContainer(this);
      instanceCache.create();
   }

   /**
    * create persistence manager
    */
   protected void createPersistenceManager() throws Exception
   {
      persistenceManager.setContainer(this);
      persistenceManager.create();
   }

   /**
    * Start persistence
    */
   protected void startPersistenceManager() throws Exception
   {
      persistenceManager.start();
   }

   /**
    * Start instance cache
    */
   protected void startInstanceCache() throws Exception
   {
      instanceCache.start();
   }

   /**
    * Stop persistence
    */
   protected void stopPersistenceManager()
   {
      persistenceManager.stop();
   }

   /**
    * Stop instance cache
    */
   protected void stopInstanceCache()
   {
      instanceCache.stop();
   }

   protected void destroyPersistenceManager()
   {
      // Destroy persistence
      persistenceManager.destroy();
      persistenceManager.setContainer(null);
   }

   protected void destroyInstanceCache()
   {
      // Destroy instance cache
      instanceCache.destroy();
      instanceCache.setContainer(null);
      try
      {
         ObjectName containerName = super.getJmxName();
         Hashtable props = containerName.getKeyPropertyList();
         props.put("plugin", "cache");
         ObjectName cacheName = new ObjectName(containerName.getDomain(), props);
         server.unregisterMBean(cacheName);
      }
      catch (Throwable ignore)
      {
      }
   }

   // EJBObject implementation --------------------------------------

   public void remove(Invocation mi)
      throws RemoteException, RemoveException
   {
      // if the session is removed already then let the user know they have a problem
      StatefulSessionEnterpriseContext ctx = (StatefulSessionEnterpriseContext) mi.getEnterpriseContext();
      if (ctx.getId() == null)
      {
         throw new RemoveException("SFSB has been removed already");
      }

      // Remove from storage
      try
      {
         AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_REMOVE);
         getPersistenceManager().removeSession(ctx);
      }
      finally
      {
         AllowedOperationsAssociation.popInMethodFlag();
      }

      // We signify "removed" with a null id
      ctx.setId(null);
      removeCount++;
   }

   // Home interface implementation ---------------------------------

   private void createSession(final Method m,
      final Object[] args,
      final StatefulSessionEnterpriseContext ctx)
      throws Exception
   {
      // Create a new ID and set it
      Object id = getPersistenceManager().createId(ctx);
      log.debug("Created new session ID: " + id);
      ctx.setId(id);

      // Invoke ejbCreate<METHOD>()
      try
      {
         AllowedOperationsAssociation.pushInMethodFlag(IN_EJB_CREATE);

         // Build the ejbCreate<METHOD> from the home create<METHOD> sig
         String createName = m.getName();
         Object instance = ctx.getInstance();
         String ejbCreateName = "ejbC" + createName.substring(1);
         Method createMethod = instance.getClass().getMethod(ejbCreateName, m.getParameterTypes());
         log.debug("Using create method for session: " + createMethod);
         createMethod.invoke(instance, args);
         createCount++;
      }
      catch (IllegalAccessException e)
      {
         ctx.setId(null);

         throw new EJBException(e);
      }
      catch (InvocationTargetException e)
      {
         ctx.setId(null);

         Throwable t = e.getTargetException();
         if (t instanceof RuntimeException)
         {
            if (t instanceof EJBException)
               throw (EJBException) t;
            // Wrap runtime exceptions
            throw new EJBException((Exception) t);
         }
         else if (t instanceof Exception)
         {
            // Remote, Create, or custom app. exception
            throw (Exception) t;
         }
         else if (t instanceof Error)
         {
            throw (Error) t;
         }
         else
         {
            throw new org.jboss.util.UnexpectedThrowable(t);
         }
      }
      finally
      {
         AllowedOperationsAssociation.popInMethodFlag();
      }

      // call back to the PM to let it know that ejbCreate has been called with success
      getPersistenceManager().createdSession(ctx);

      // Insert in cache
      getInstanceCache().insert(ctx);

      // Create EJBObject
      if (getProxyFactory() != null)
         ctx.setEJBObject((EJBObject) getProxyFactory().getStatefulSessionEJBObject(id));

      // Create EJBLocalObject
      if (getLocalHomeClass() != null)
         ctx.setEJBLocalObject(getLocalProxyFactory().getStatefulSessionEJBLocalObject(id));
   }

   public EJBObject createHome(Invocation mi)
      throws Exception
   {
      StatefulSessionEnterpriseContext ctx = (StatefulSessionEnterpriseContext) mi.getEnterpriseContext();
      createSession(mi.getMethod(), mi.getArguments(), ctx);
      return ctx.getEJBObject();
   }


   // local home interface implementation

   /**
    * @throws Error Not yet implemented
    */
   public void removeLocalHome(Invocation mi)
      throws RemoteException, RemoveException
   {
      throw new UnreachableStatementException();
   }

   public EJBLocalObject createLocalHome(Invocation mi)
      throws Exception
   {
      StatefulSessionEnterpriseContext ctx = (StatefulSessionEnterpriseContext) mi.getEnterpriseContext();
      createSession(mi.getMethod(), mi.getArguments(), ctx);
      return ctx.getEJBLocalObject();
   }

   /**
    * A method for the getEJBObject from the handle
    */
   public EJBObject getEJBObject(Invocation mi) throws RemoteException
   {
      // All we need is an EJBObject for this Id, the first argument is the Id
      EJBProxyFactory ci = getProxyFactory();
      if (ci == null)
      {
         String msg = "No ProxyFactory, check for ProxyFactoryFinderInterceptor";
         throw new IllegalStateException(msg);
      }

      Object id = mi.getArguments()[0];
      if (id == null)
         throw new IllegalStateException("Cannot get a session interface with a null id");

      // Does the session still exist?
      InstanceCache cache = getInstanceCache();
      BeanLock lock = getLockManager().getLock(id);
      lock.sync();
      try
      {
         if (cache.get(id) == null)
            throw new RemoteException("Session no longer exists: " + id);
      }
      finally
      {
         lock.releaseSync();
         getLockManager().removeLockRef(id);
      }

      // Ok lets create the proxy
      return (EJBObject) ci.getStatefulSessionEJBObject(id);
   }


   // EJBHome implementation ----------------------------------------

   //
   // These are implemented in the local proxy
   //

   /**
    * @throws Error Not yet implemented
    */
   public void removeHome(Invocation mi)
      throws RemoteException, RemoveException
   {
      throw new Error("Not Yet Implemented");
   }

   public String getBeanTypeName()
   {
      return "StatefulSession";
   }

   // Private -------------------------------------------------------

   protected void setupHomeMapping() throws Exception
   {
      // Adrian Brock: This should go away when we don't support EJB1x
      boolean isEJB1x = metaData.getApplicationMetaData().isEJB1x();

      Map map = new HashMap();

      if (homeInterface != null)
      {

         Method[] m = homeInterface.getMethods();
         for (int i = 0; i < m.length; i++)
         {
            try
            {
               // Implemented by container
               if (isEJB1x == false && m[i].getName().startsWith("create"))
               {
                  map.put(m[i], getClass().getMethod("createHome",
                     new Class[]{Invocation.class}));
               }
               else
               {
                  map.put(m[i], getClass().getMethod(m[i].getName() + "Home",
                     new Class[]{Invocation.class}));
               }
            }
            catch (NoSuchMethodException e)
            {
               log.info(m[i].getName() + " in bean has not been mapped");
            }
         }
      }

      if (localHomeInterface != null)
      {
         Method[] m = localHomeInterface.getMethods();
         for (int i = 0; i < m.length; i++)
         {
            try
            {
               // Implemented by container
               if (isEJB1x == false && m[i].getName().startsWith("create"))
               {
                  map.put(m[i], getClass().getMethod("createLocalHome",
                     new Class[]{Invocation.class}));
               }
               else
               {
                  map.put(m[i], getClass().getMethod(m[i].getName() + "LocalHome",
                     new Class[]{Invocation.class}));
               }
            }
            catch (NoSuchMethodException e)
            {
               log.info(m[i].getName() + " in bean has not been mapped");
            }
         }
      }

      try
      {
         // Get getEJBObject from on Handle, first get the class
         Class handleClass = Class.forName("javax.ejb.Handle");

         //Get only the one called handle.getEJBObject
         Method getEJBObjectMethod = handleClass.getMethod("getEJBObject", new Class[0]);

         //Map it in the home stuff
         map.put(getEJBObjectMethod, getClass().getMethod("getEJBObject",
            new Class[]{Invocation.class}));
      }
      catch (NoSuchMethodException e)
      {
         log.debug("Couldn't find getEJBObject method on container");
      }

      homeMapping = map;
   }

   protected Interceptor createContainerInterceptor()
   {
      return new ContainerInterceptor();
   }

   /**
    * This is the last step before invocation - all interceptors are done
    */
   class ContainerInterceptor
      extends AbstractContainerInterceptor
   {
      public Object invokeHome(Invocation mi) throws Exception
      {
         boolean trace = log.isTraceEnabled();

         if (trace)
         {
            log.trace("HOMEMETHOD coming in ");
            log.trace("" + mi.getMethod());
            log.trace("HOMEMETHOD coming in hashcode" + mi.getMethod().hashCode());
            log.trace("HOMEMETHOD coming in classloader" + mi.getMethod().getDeclaringClass().getClassLoader().hashCode());
            log.trace("CONTAINS " + getHomeMapping().containsKey(mi.getMethod()));
         }

         Method miMethod = mi.getMethod();
         Method m = (Method) getHomeMapping().get(miMethod);
         if (m == null)
         {
            String msg = "Invalid invocation, check your deployment packaging"
               + ", method=" + miMethod;
            throw new EJBException(msg);
         }

         // Invoke and handle exceptions
         if (trace)
         {
            log.trace("HOMEMETHOD m " + m);
            java.util.Iterator iterator = getHomeMapping().keySet().iterator();
            while (iterator.hasNext())
            {
               Method me = (Method) iterator.next();

               if (me.getName().endsWith("create"))
               {
                  log.trace(me.toString());
                  log.trace("" + me.hashCode());
                  log.trace("" + me.getDeclaringClass().getClassLoader().hashCode());
                  log.trace("equals " + me.equals(mi.getMethod()) + " " + mi.getMethod().equals(me));
               }
            }
         }

         try
         {
            return mi.performCall(StatefulSessionContainer.this, m, new Object[]{mi});
         }
         catch (Exception e)
         {
            rethrow(e);
         }

         // We will never get this far, but the compiler does not know that
         throw new org.jboss.util.UnreachableStatementException();
      }

      public Object invoke(Invocation mi) throws Exception
      {
         // Get method
         Method miMethod = mi.getMethod();
         Method m = (Method) getBeanMapping().get(miMethod);
         if (m == null)
         {
            String msg = "Invalid invocation, check your deployment packaging"
               + ", method=" + miMethod;
            throw new EJBException(msg);
         }

         // wire the transaction on the context, this is how the instance remember the tx
         // Unlike Entity beans we can't do that in the previous interceptors (ordering)
         EnterpriseContext ctx = (EnterpriseContext) mi.getEnterpriseContext();
         if (ctx.getTransaction() == null)
            ctx.setTransaction(mi.getTransaction());
         else if(ejbObjectRemove.equals(miMethod) || ejbLocalObjectRemove.equals(miMethod))
         {
            throw new RemoveException("An attempt to remove a session " +
               "object while the object is in a transaction " +
               "(EJB2.1, 7.6.4 Restrictions for Transactions): ejb-name=" +
               metaData.getEjbName() + ", method=" + mi.getMethod() + ", tx=" + ctx.getTransaction());
         }

         // Select instance to invoke (container or bean)
         if (m.getDeclaringClass().equals(StatefulSessionContainer.class)
            || m.getDeclaringClass().equals(SessionContainer.class))
         {
            // Invoke and handle exceptions
            try
            {
               return mi.performCall(StatefulSessionContainer.this, m, new Object[]{mi});
            }
            catch (Exception e)
            {
               rethrow(e);
            }
         }
         else
         {
            // Invoke and handle exceptions
            try
            {
               Object bean = ctx.getInstance();
               return mi.performCall(bean, m, mi.getArguments());
            }
            catch (Exception e)
            {
               rethrow(e);
            }
         }

         // We will never get this far, but the compiler does not know that
         throw new org.jboss.util.UnreachableStatementException();
      }
   }
}
