/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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;

// $Id: SessionContainer.java 57209 2006-09-26 12:21:57Z dimitris@jboss.org $

import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

import javax.ejb.EJBHome;
import javax.ejb.EJBLocalHome;
import javax.ejb.EJBMetaData;
import javax.ejb.EJBObject;
import javax.ejb.Handle;
import javax.ejb.HomeHandle;
import javax.ejb.RemoveException;
import javax.ejb.TimedObject;
import javax.ejb.Timer;
import javax.management.ObjectName;

import org.jboss.invocation.Invocation;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.metadata.SessionMetaData;

/**
 * <p>
 * Container dedicated to session beans. Contains factored out
 * redundancies between stateless and stateful treatments, because
 * (extending the spec) we would like to also support stateful
 * web services.
 * </p>
 * @author <a href="mailto:Christoph.Jung@infor.de">Christoph G. Jung</a>
 * @version $Revision: 57209 $
 * @since 30.10.2003
 */
public abstract class SessionContainer extends Container
{
   /**
    * These are the mappings between the home interface methods and the
    * container methods.
    */
   protected Map homeMapping;

   /**
    * These are the mappings between the remote interface methods and the
    * bean methods.
    */
   protected Map beanMapping;

   /**
    * This is the first interceptor in the chain. The last interceptor must
    * be provided by the container itself
    */
   protected Interceptor interceptor;

   /** this is the service endpoint class */
   protected Class serviceEndpoint;

   /** This is the instancepool that is to be used */
   protected InstancePool instancePool;

   /** set the instance pool */
   public void setInstancePool(InstancePool ip)
   {
      if (ip == null)
         throw new IllegalArgumentException("Null pool");

      this.instancePool = ip;
      ip.setContainer(this);
   }

   /** return instance pool */
   public InstancePool getInstancePool()
   {
      return instancePool;
   }

   /** return local proxy factory */
   public LocalProxyFactory getLocalProxyFactory()
   {
      return localProxyFactory;
   }

   /** add an additional interceptor to the chain */
   public void addInterceptor(Interceptor in)
   {
      if (interceptor == null)
      {
         interceptor = in;
      }
      else
      {
         Interceptor current = interceptor;
         while (current.getNext() != null)
         {
            current = current.getNext();
         }

         current.setNext(in);
      }
   }

   /** return first interceptor */
   public Interceptor getInterceptor()
   {
      return interceptor;
   }

   /** return service endpoint */
   public Class getServiceEndpoint()
   {
      return serviceEndpoint;
   }

   // Container stuff

   protected void createService() throws Exception
   {
      // Associate thread with classloader
      ClassLoader oldCl = SecurityActions.getContextClassLoader();
      SecurityActions.setContextClassLoader(getClassLoader());

      try
      {
         // Acquire classes from CL
         if (metaData.getHome() != null)
            homeInterface = classLoader.loadClass(metaData.getHome());
         if (metaData.getRemote() != null)
            remoteInterface = classLoader.loadClass(metaData.getRemote());
         if (((SessionMetaData) metaData).getServiceEndpoint() != null)
         {
            serviceEndpoint =
                    classLoader.loadClass(((SessionMetaData) metaData).getServiceEndpoint());
         }

         // Call default init
         super.createService();

         // Make some additional validity checks with regards to the container configuration
         checkCoherency();

         // Map the bean methods
         setupBeanMapping();

         // Map the home methods
         setupHomeMapping();

         // Map the interfaces to Long
         setupMarshalledInvocationMapping();

         createInvokers();

         createInstanceCache();

         createInstancePool();

         createPersistenceManager();

         createInterceptors();
      }
      finally
      {
         // Reset classloader
         SecurityActions.setContextClassLoader(oldCl);
      }
   }

   /**
    * how home methods are treated by container
    */
   protected abstract void setupHomeMapping() throws Exception;

   /** loop through methods and setup mapping */
   protected void setUpBeanMappingImpl(Map map, Method[] methods, String declaringClass)
           throws NoSuchMethodException
   {
      for (int i = 0; i < methods.length; i++)
      {
         Method m = methods[i];
         if (m.getDeclaringClass().getName().equals(declaringClass) == false)
         {
            // Implemented by bean
            try
            {
               Method beanMethod = beanClass.getMethod(m.getName(), m.getParameterTypes());
               map.put(m, beanMethod);
            }
            catch (NoSuchMethodException ex)
            {
               throw new NoSuchMethodException("Not found in bean class: " + m);
            }

            if (log.isDebugEnabled())
               log.debug("Mapped " + m.getName() + " HASH " + m.hashCode() + "to " + map.get(m));
         }
         else
         {
            // Implemented by container
            try
            {
               Method containerMethod = getClass().getMethod(m.getName(), new Class[]{Invocation.class});
               map.put(m, containerMethod);
            }
            catch (NoSuchMethodException e)
            {
               throw new NoSuchMethodException("Not found in container class: " + m);
            }

            if (log.isDebugEnabled())
               log.debug("Mapped Container method " + m.getName() + " HASH " + m.hashCode());
         }
      }
   }

   /** build bean mappings for application logic */
   protected void setupBeanMapping() throws NoSuchMethodException
   {
      Map map = new HashMap();

      if (remoteInterface != null)
      {
         Method[] m = remoteInterface.getMethods();
         setUpBeanMappingImpl(map, m, "javax.ejb.EJBObject");
      }

      if (localInterface != null)
      {
         Method[] m = localInterface.getMethods();
         setUpBeanMappingImpl(map, m, "javax.ejb.EJBLocalObject");
      }

      if (TimedObject.class.isAssignableFrom(beanClass))
      {
         Method[] m = new Method[]{TimedObject.class.getMethod("ejbTimeout", new Class[]{Timer.class})};
         setUpBeanMappingImpl(map, m, "javax.ejb.Timer");
      }

      if (serviceEndpoint != null)
      {
         Method[] m = serviceEndpoint.getMethods();
         setUpBeanMappingImpl(map, m, "java.rmi.Remote");
      }

      beanMapping = map;
   }

   /**
    * sets up marshalled invocation mappings
    * @throws Exception
    */

   protected void setupMarshalledInvocationMapping() throws Exception
   {
      // Create method mappings for container invoker
      if (homeInterface != null)
      {
         Method[] m = homeInterface.getMethods();
         for (int i = 0; i < m.length; i++)
         {
            marshalledInvocationMapping.put(new Long(MarshalledInvocation.calculateHash(m[i])), m[i]);
         }
      }

      if (remoteInterface != null)
      {
         Method[] m = remoteInterface.getMethods();
         for (int j = 0; j < m.length; j++)
         {
            marshalledInvocationMapping.put(new Long(MarshalledInvocation.calculateHash(m[j])), m[j]);
         }
      }
      // Get the getEJBObjectMethod
      Method getEJBObjectMethod =
              Class.forName("javax.ejb.Handle").getMethod("getEJBObject",
                      new Class[0]);

      // Hash it
      marshalledInvocationMapping.put(new Long(MarshalledInvocation.calculateHash(getEJBObjectMethod)), getEJBObjectMethod);
   }

   protected void checkCoherency() throws Exception
   {
      // Check clustering cohrency wrt metadata
      //
      if (metaData.isClustered())
      {
         boolean clusteredProxyFactoryFound = false;
         Iterator it = proxyFactories.keySet().iterator();
         while (it.hasNext())
         {
            String invokerBinding = (String) it.next();
            EJBProxyFactory ci = (EJBProxyFactory) proxyFactories.get(invokerBinding);
            if (ci instanceof org.jboss.proxy.ejb.ClusterProxyFactory)
               clusteredProxyFactoryFound = true;
         }

         if (!clusteredProxyFactoryFound)
         {
            log.warn("*** EJB '"
                    + this.metaData.getEjbName()
                    + "' deployed as CLUSTERED but not a single clustered-invoker is bound to container ***");
         }
      }
   }

   /** creates a new instance pool */
   protected void createInstancePool() throws Exception
   {

      // Try to register the instance pool as an MBean
      try
      {
         ObjectName containerName = super.getJmxName();
         Hashtable props = containerName.getKeyPropertyList();
         props.put("plugin", "pool");
         ObjectName poolName = new ObjectName(containerName.getDomain(), props);
         server.registerMBean(instancePool, poolName);
      }
      catch (Throwable t)
      {
         log.debug("Failed to register pool as mbean", t);
      }
      // Initialize pool
      instancePool.create();
   }

   /**
    * no instance cache per default
    */
   protected void createInstanceCache() throws Exception
   {
   }

   /** creates the invokers */
   protected void createInvokers() throws Exception
   {
      // Init container invoker
      for (Iterator it = proxyFactories.keySet().iterator(); it.hasNext();)
      {
         String invokerBinding = (String) it.next();
         EJBProxyFactory ci = (EJBProxyFactory) proxyFactories.get(invokerBinding);
         ci.create();
      }
   }

   /** Initialize the interceptors by calling the chain */
   protected void createInterceptors() throws Exception
   {
      Interceptor in = interceptor;
      while (in != null)
      {
         in.setContainer(this);
         in.create();
         in = in.getNext();
      }
   }

   /**
    * no persistence manager per default
    */
   protected void createPersistenceManager() throws Exception
   {
   }

   protected void startService() throws Exception
   {
      // Associate thread with classloader
      ClassLoader oldCl = SecurityActions.getContextClassLoader();
      SecurityActions.setContextClassLoader(getClassLoader());

      try
      {
         // Call default start
         super.startService();

         startInvokers();

         startInstanceCache();

         startInstancePool();

         startPersistenceManager();

         startInterceptors();
         
         // Restore persisted ejb timers
         restoreTimers();         
      }
      finally
      {
         // Reset classloader
         SecurityActions.setContextClassLoader(oldCl);
      }
   }

   /**
    * no persistence manager per default
    */
   protected void startPersistenceManager() throws Exception
   {
   }

   /**
    * no instance cache per default
    */
   protected void startInstanceCache() throws Exception
   {
   }

   /** Start container invokers */
   protected void startInvokers() throws Exception
   {
      for (Iterator it = proxyFactories.keySet().iterator(); it.hasNext();)
      {
         String invokerBinding = (String) it.next();
         EJBProxyFactory ci = (EJBProxyFactory) proxyFactories.get(invokerBinding);
         ci.start();
      }
   }

   /** Start pool */
   protected void startInstancePool() throws Exception
   {
      instancePool.start();
   }

   /** Start all interceptors in the chain **/
   protected void startInterceptors() throws Exception
   {
      Interceptor in = interceptor;
      while (in != null)
      {
         in.start();
         in = in.getNext();
      }
   }

   protected void stopService() throws Exception
   {
      // Associate thread with classloader
      ClassLoader oldCl = SecurityActions.getContextClassLoader();
      SecurityActions.setContextClassLoader(getClassLoader());

      try
      {
         // Call default stop
         super.stopService();

         stopInvokers();

         stopInstanceCache();

         stopInstancePool();

         stopPersistenceManager();

         stopInterceptors();
      }
      finally
      {
         // Reset classloader
         SecurityActions.setContextClassLoader(oldCl);
      }
   }

   /** Stop all interceptors in the chain */
   protected void stopInterceptors()
   {
      Interceptor in = interceptor;
      while (in != null)
      {
         in.stop();
         in = in.getNext();
      }
   }

   /** no persistence */
   protected void stopPersistenceManager()
   {
   }

   /** Stop pool */
   protected void stopInstancePool()
   {
      instancePool.stop();
   }

   /** no instance cache */
   protected void stopInstanceCache()
   {
   }

   /** Stop container invoker */
   protected void stopInvokers()
   {
      for (Iterator it = proxyFactories.keySet().iterator(); it.hasNext();)
      {
         String invokerBinding = (String) it.next();
         EJBProxyFactory ci = (EJBProxyFactory) proxyFactories.get(invokerBinding);
         ci.stop();
      }
   }

   protected void destroyService() throws Exception
   {
      // Associate thread with classloader
      ClassLoader oldCl = SecurityActions.getContextClassLoader();
      SecurityActions.setContextClassLoader(getClassLoader());

      try
      {
         destroyInvokers();

         destroyInstanceCache();

         destroyInstancePool();

         destroyPersistenceManager();

         destroyInterceptors();

         destroyMarshalledInvocationMapping();
         
        homeInterface = null;
        remoteInterface = null;
        serviceEndpoint = null;
        beanMapping.clear();

         // Call default destroy
         super.destroyService();
      }
      finally
      {
         // Reset classloader
         SecurityActions.setContextClassLoader(oldCl);
      }
   }

   protected void destroyMarshalledInvocationMapping()
   {
      MarshalledInvocation.removeHashes(homeInterface);
      MarshalledInvocation.removeHashes(remoteInterface);
   }

   protected void destroyInterceptors()
   {
      // Destroy all the interceptors in the chain
      Interceptor in = interceptor;
      while (in != null)
      {
         in.destroy();
         in.setContainer(null);
         in = in.getNext();
      }
   }

   protected void destroyPersistenceManager()
   {
   }

   protected void destroyInstancePool()
   {
      // Destroy pool
      instancePool.destroy();
      instancePool.setContainer(null);
      try
      {
         ObjectName containerName = super.getJmxName();
         Hashtable props = containerName.getKeyPropertyList();
         props.put("plugin", "pool");
         ObjectName poolName = new ObjectName(containerName.getDomain(), props);
         server.unregisterMBean(poolName);
      }
      catch (Throwable ignore)
      {
      }
   }

   protected void destroyInstanceCache()
   {
   }

   protected void destroyInvokers()
   {
      // Destroy container invoker
      for (Iterator it = proxyFactories.keySet().iterator(); it.hasNext();)
      {
         String invokerBinding = (String) it.next();
         EJBProxyFactory ci = (EJBProxyFactory) proxyFactories.get(invokerBinding);
         ci.destroy();
         ci.setContainer(null);
      }
   }

   public Object internalInvokeHome(Invocation mi) throws Exception
   {
      Method method = mi.getMethod();
      if (method != null && method.getName().equals("remove"))
      {
         // Handle or primary key?
         Object arg = mi.getArguments()[0];
         if (arg instanceof Handle)
         {
            if (arg == null)
               throw new RemoteException("Null handle");
            Handle handle = (Handle) arg;
            EJBObject ejbObject = handle.getEJBObject();
            ejbObject.remove();
            return null;
         }
         else
            throw new RemoveException("EJBHome.remove(Object) not allowed for session beans");
      }
      return getInterceptor().invokeHome(mi);
   }

   /**
    * This method does invocation interpositioning of tx and security,
    * retrieves the instance from an object table, and invokes the method
    * on the particular instance
    */
   public Object internalInvoke(Invocation mi) throws Exception
   {
      // Invoke through interceptors
      return getInterceptor().invoke(mi);
   }

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

   /**
    * While the following methods are implemented in the client in the case
    * of JRMP we would need to implement them to fully support other transport
    * protocols
    *
    * @return  Always null
    */
   public Handle getHandle(Invocation mi) throws RemoteException
   {
      
      // TODO
      return null;
   }

   public Object getPrimaryKey(Invocation mi) throws RemoteException
   {
      return getPrimaryKey();
   }

   public Object getPrimaryKey() throws RemoteException
   {
      throw new RemoteException("Call to getPrimaryKey not allowed on session bean");
   }
   
   public EJBHome getEJBHome(Invocation mi) throws RemoteException
   {
      EJBProxyFactory ci = getProxyFactory();
      if (ci == null)
      {
         String msg = "No ProxyFactory, check for ProxyFactoryFinderInterceptor";
         throw new IllegalStateException(msg);
      }

      return (EJBHome) ci.getEJBHome();
   }

   /**
    * @return   Always false
    */
   public boolean isIdentical(Invocation mi) throws RemoteException
   {
      EJBProxyFactory ci = getProxyFactory();
      if (ci == null)
      {
         String msg = "No ProxyFactory, check for ProxyFactoryFinderInterceptor";
         throw new IllegalStateException(msg);
      }

      return ci.isIdentical(this, mi);
   }

   public EJBMetaData getEJBMetaDataHome(Invocation mi) throws RemoteException
   {
      return getEJBMetaDataHome();
   }
   
   public EJBMetaData getEJBMetaDataHome() throws RemoteException
   {
      EJBProxyFactory ci = getProxyFactory();
      if (ci == null)
      {
         String msg = "No ProxyFactory, check for ProxyFactoryFinderInterceptor";
         throw new IllegalStateException(msg);
      }

      return ci.getEJBMetaData();
   }
   
   public HomeHandle getHomeHandleHome(Invocation mi) throws RemoteException
   {
      return getHomeHandleHome();
   }
   
   public HomeHandle getHomeHandleHome() throws RemoteException
   {
      EJBProxyFactory ci = getProxyFactory();
      if (ci == null)
      {
         String msg = "No ProxyFactory, check for ProxyFactoryFinderInterceptor";
         throw new IllegalStateException(msg);
      }

      EJBHome home = (EJBHome) ci.getEJBHome();
      return home.getHomeHandle();
   }

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

   // local object interface implementation

   public EJBLocalHome getEJBLocalHome(Invocation mi)
   {
      return localProxyFactory.getEJBLocalHome();
   }

   /**
    * needed for sub-inner-class access (old jdk compiler bug)
    * @return
    */
   protected Map getHomeMapping()
   {
      return homeMapping;
   }

   /**
    * needed for sub-inner-class access (old jdk compiler bug)
    * @return
    */
   protected Map getBeanMapping()
   {
      return beanMapping;
   }

}
