package example.interceptor.factory;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.hivemind.InterceptorStack;
import org.apache.hivemind.ServiceInterceptorFactory;
import org.apache.hivemind.impl.BaseLocatable;
import org.apache.hivemind.internal.Module;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

/**
 * This class provides an easy mechanism for adding AOP Alliance
 * MethodInterceptors to services in HiveMind.  The methodInterceptor
 * property may be set via BuilderFactory as another service or just
 * a bean implementing the MethodInterceptor interface.
 *
 * @author James Carman
 * @version 1.0
 */
public final class MethodInterceptorFactory extends BaseLocatable implements ServiceInterceptorFactory
{
// ------------------------------ FIELDS ------------------------------

    private MethodInterceptor methodInterceptor;

// --------------------- GETTER / SETTER METHODS ---------------------

    public void setMethodInterceptor( MethodInterceptor methodInterceptor )
    {
        this.methodInterceptor = methodInterceptor;
    }

// ------------------------ INTERFACE METHODS ------------------------


// --------------------- Interface ServiceInterceptorFactory ---------------------

    /**
     * Creates the service interceptor using the MethodInterceptor supplied by
     * the subclass.
     *
     * @param interceptorStack the interceptor stack
     * @param module           the contributing module
     * @param list             configuration parameters
     */
    public final void createInterceptor( InterceptorStack interceptorStack, Module module, List list )
    {
        final Class[] interfaces = new Class[]{interceptorStack.getServiceInterface()};
        final ClassLoader classLoader = module.getClassResolver().getClassLoader();
        final InvocationHandler invocationHandler = new MethodInterceptorInvocationHandler( interceptorStack );
        interceptorStack.push( Proxy.newProxyInstance( classLoader, interfaces, invocationHandler ) );
    }

// -------------------------- INNER CLASSES --------------------------

    /**
     * A java proxy InvocationHandler implementation which allows a MethodInterceptor to intercept the method invocation.
     */
    private final class MethodInterceptorInvocationHandler implements InvocationHandler
    {
        private final InterceptorStack stack;
        private final Object target;

        /**
         * Constructs a MethodInterceptorInvocationHandler
         *
         * @param stack the interceptor stack
         */
        public MethodInterceptorInvocationHandler( InterceptorStack stack )
        {
            this.stack = stack;
            this.target = stack.peek();
        }

        /**
         * Calls the MethodInterceptor's invoke method.
         *
         * @param proxy  a reference to the proxy instance
         * @param method the method being invoked
         * @param args   the arguments to the method
         * @return the value returned by the MethodInterceptor
         * @throws Throwable
         */
        public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
        {
            return methodInterceptor.invoke( new MethodInvocationImpl( target, method, args, stack.peek() ) );
        }
    }

    /**
     * A java reflection-based implementation of a MethodInvocation
     */
    private final class MethodInvocationImpl implements MethodInvocation
    {
        private final Object next;
        private final Method method;
        private final Object[] arguments;
        private final Object proxy;

        /**
         * Constructs a MethodInvocationImpl object.
         *
         * @param next      the next object
         * @param method    the method
         * @param arguments the arguments
         * @param proxy     the outermost proxy object (allows calling another method instead).
         */
        public MethodInvocationImpl( Object next, Method method, Object[] arguments, Object proxy )
        {
            this.next = next;
            this.method = method;
            this.arguments = arguments;
            this.proxy = proxy;
        }

        /**
         * Invokes the method on the next object.
         *
         * @return value returned by invoking the method on the next object
         * @throws Throwable throwable thrown by invoking method on the next object
         */
        public final Object proceed() throws Throwable
        {
            try
            {
                return method.invoke( next, arguments );
            }
            catch( InvocationTargetException e )
            {
                throw e.getTargetException();
            }
        }

        public final Method getMethod()
        {
            return method;
        }

        public final AccessibleObject getStaticPart()
        {
            return method;
        }

        public final Object getThis()
        {
            return proxy;
        }

        public final Object[] getArguments()
        {
            return arguments;
        }
    }
}
