/*
 * JBoss, the OpenSource EJB server
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.ejb3.interceptor;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import javax.ejb.AroundInvoke;
import javax.ejb.InvocationContext;
import org.jboss.aop.Advisor;
import org.jboss.aop.InstanceAdvisor;
import org.jboss.aop.joinpoint.Joinpoint;
import org.jboss.ejb3.Container;
import org.jboss.logging.Logger;

/**
 * @author <a href="mailto:kabir.khan@jboss.org">Kabir Khan</a>
 * @version $Revision: 1.4.2.3 $
 */
public class EJB3InterceptorsFactory implements org.jboss.aop.advice.AspectFactory
{

   static Logger log = Logger.getLogger(EJB3InterceptorsFactory.class);

   public String getName()
   {
      return getClass().getName();
   }

   public Object createPerVM()
   {
      throw new RuntimeException("NOT ALLOWED");
   }

   public Object createPerInstance(Advisor advisor, InstanceAdvisor instanceAdvisor)
   {
      throw new RuntimeException("NOT ALLOWED");
   }

   public Object createPerJoinpoint(Advisor advisor, Joinpoint jp)
   {
      throw new RuntimeException("NOT ALLOWED");
   }

   public Object createPerJoinpoint(Advisor advisor, InstanceAdvisor instanceAdvisor, Joinpoint jp)
   {
      throw new RuntimeException("NOT ALLOWED");
   }

   public Object createPerClass(Advisor advisor)
   {
      Container container = (Container) advisor;
      Class beanClass = container.getBeanClass();

      ArrayList ejbInterceptors = new ArrayList();
      ArrayList<Method> aroundInvokes = new ArrayList<Method>();

      //First add all the interceptors from the interceptor classes
      javax.ejb.Interceptors interceptors = (javax.ejb.Interceptors) advisor.resolveAnnotation(javax.ejb.Interceptors.class);

      if (interceptors != null)
      {
         try
         {
            createInterceptors(container, ejbInterceptors, aroundInvokes, interceptors);
         }
         catch (Exception e)
         {
            throw new RuntimeException(e);
         }
      }
      else
      {
         javax.ejb.Interceptor interceptor = (javax.ejb.Interceptor) advisor.resolveAnnotation(javax.ejb.Interceptor.class);
         if (interceptor != null)
         {
            try
            {
               createInterceptors(container, ejbInterceptors, aroundInvokes, interceptor);
            }
            catch (Exception e)
            {
               throw new RuntimeException(e);
            }
         }
      }
      Method beanAroundInvoke = findBeanClassAroundInvoke(advisor, beanClass);
      Object[] ints = ejbInterceptors.toArray(new Object[ejbInterceptors.size()]);
      Method[] arounds = aroundInvokes.toArray(new Method[aroundInvokes.size()]);
      return new EJB3InterceptorsInterceptor(ints, arounds, beanAroundInvoke);
   }

   private static void createInterceptors(Container container, ArrayList ejbInterceptors, ArrayList<Method> aroundInvokes, javax.ejb.Interceptors interceptors) throws Exception
   {
      for (Class interceptor : interceptors.value())
      {
         createInterceptors(container, ejbInterceptors, aroundInvokes, interceptor);
      }
   }

   private static void createInterceptors(Container container, ArrayList ejbInterceptors, ArrayList<Method> aroundInvokes, javax.ejb.Interceptor interceptor) throws Exception
   {
      createInterceptors(container, ejbInterceptors, aroundInvokes, interceptor.value());
   }

   private static void createInterceptors(Container container, ArrayList ejbInterceptors, ArrayList<Method> aroundInvokes, Class intClass) throws Exception
   {
      Class clazz = intClass;
      Method found = null;
      for (Method method : clazz.getMethods())
      {
         AroundInvoke ai = (AroundInvoke) method.getAnnotation(AroundInvoke.class);
         if (ai != null)
         {
            if (found == null)
            {
               found = method;

               if (checkMethodSignature(method))
               {
                  ejbInterceptors.add(clazz.newInstance());
                  aroundInvokes.add(method);
               }
               else
               {
                  throw new RuntimeException("@Around annotated method: " + method.getName() + " in " + clazz
                                             + " has a wrong method signature, and will be ignored");
               }
            }
            else
            {
               throw new RuntimeException("More than one @AroundInvoke annotated method was found in " + clazz.getName() +
                                          ". Ignoring all except: " + method.getName());
            }
         }
      }
   }

   private static Method findBeanClassAroundInvoke(Advisor advisor, Class beanClass)
   {
      Method found = null;
      for (Method method : beanClass.getMethods())
      {
         AroundInvoke ai = (AroundInvoke) advisor.resolveAnnotation(method, AroundInvoke.class);
         if (ai != null)
         {
            if (found == null)
            {
               found = method;

               if (!checkMethodSignature(method))
               {
                  throw new RuntimeException("@Around annotated method: " + method.getName() + " in " + beanClass
                                             + " has a wrong method signature, and will be ignored");
               }
            }
            else
            {
               throw new RuntimeException("More than one @AroundInvoke annotated method was found in " + beanClass.getName() +
                                          ". Ignoring all except: " + method.getName());
            }
         }
      }
      return found;
   }

   private static boolean checkMethodSignature(Method method)
   {
      int modifiers = method.getModifiers();

      if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers))
      {
         if (method.getReturnType().equals(Object.class))
         {
            Class[] params = method.getParameterTypes();
            if (params.length == 1 && params[0].equals(InvocationContext.class))
            {
               Class[] exceptions = method.getExceptionTypes();
               if (exceptions.length == 1 && exceptions[0].equals(Exception.class))
               {
                  return true;
               }
            }
         }
      }

      return false;
   }
}
