/*
 * Copyright (C) The MX4J Contributors.
 * All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0.
 * See the terms of the MX4J License in the documentation provided with this software.
 */

package mx4j.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import javax.management.Attribute;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeErrorException;
import javax.management.RuntimeMBeanException;
import javax.management.RuntimeOperationsException;

/**
 * A utility class that creates proxies for invocation on standard MBeans (does not work for DynamicMBeans) on local MBeanServers. <br>
 * Usage example:
 * <pre>
 * public interface MyServiceMBean {...}
 * public class MyService implements MyServiceMBean {...}
 * pulic class Main
 * {
 *    public static void main(String[] args) throws Exception
 *    {
 *       MBeanServer server = ...;
 *       ObjectName myServiceObjectName = ...;
 * <p/>
 *       MyServiceMBean mbean = (MyServiceMBean)StandardMBeanProxy.create(MyServiceMBean.class, server, myServiceObjectName);
 * <p/>
 *       ...
 *    }
 * }
 * </pre>
 *
 * @version $Revision: 1.10 $
 * @deprecated Replaced by {@link javax.management.MBeanServerInvocationHandler}
 */
public class StandardMBeanProxy
{
   /**
    * Creates a proxy with the given MBean interface for an MBean with the specified name
    * living in the MBeanServer returned by <code>MBeanServerFactory.findMBeanServer(null).get(0)</code>.
    *
    * @deprecated Replaced by {@link #create(Class mbeanInterface, MBeanServer server, ObjectName name)}
    */
   public static Object create(Class mbeanInterface, ObjectName name)
   {
      return create(mbeanInterface, null, name);
   }

   /**
    * Creates a proxy with the given MBean interface for an MBean with the specified ObjectName
    * living in the specified local MBeanServer. <br>
    * Calling this method when the given ObjectName does not represent a registered MBean
    * in the given MBeanServer results in an exception being thrown.
    * If the MBean is unregistered after the proxy has been created, an attempt to call any method
    * on the proxy will result in a {@link java.lang.reflect.UndeclaredThrowableException UndeclaredThrowableException}
    * being thrown. MBeanServer's behavior would be to throw an InstanceNotFoundException, but this exception
    * is normally not declared in the throws clause of MBean's management interface, thus resulting
    * in the UndeclaredThrowableException being thrown instead.
    */
   public static Object create(Class mbeanInterface, MBeanServer server, ObjectName name)
   {
      if (mbeanInterface == null) throw new IllegalArgumentException("MBean interface cannot be null");
      if (!mbeanInterface.isInterface()) throw new IllegalArgumentException("Class parameter must be an interface");
      if (name == null) throw new IllegalArgumentException("MBean name cannot be null");

      if (server == null)
      {
         List list = MBeanServerFactory.findMBeanServer(null);
         if (list.size() > 0)
            server = (MBeanServer)list.get(0);
         else
            throw new IllegalArgumentException("Cannot find MBeanServer");
      }

      // Do this check to reduce InstanceNotFoundExceptions, that are rethrown as UndeclaredThrowableException
      // and thus are not easily caught by clients.
      if (!server.isRegistered(name)) throw new IllegalArgumentException("ObjectName " + name + " is not known to MBeanServer " + server);

      // The client must be able to cast the returned object to the mbeanInterface it passes,
      // so the classloader must be the same
      ClassLoader loader = mbeanInterface.getClassLoader();
      return Proxy.newProxyInstance(loader, new Class[]{mbeanInterface}, new LocalHandler(server, name));
   }

   /**
    * Base class for MBeanServer invocation handlers. <br>
    * It handles the translation of method invocation
    * from the definition of reflection (java.lang.reflect.Method) to
    * the definition of JMX (method name + signature)
    */
   protected static abstract class Handler implements InvocationHandler
   {
      protected Handler()
      {
      }

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
      {
         // Needed for no-parameter methods
         if (args == null) args = new Object[0];

         // No need to check for consistency between the signature and the args parameter,
         // since the invocation it is not done by reflection, but statically

         Class[] declared = method.getExceptionTypes();

         if (Utils.isAttributeSetter(method))
         {
            String name = method.getName().substring(3);
            Attribute attribute = new Attribute(name, args[0]);
            try
            {
               setAttribute(attribute);
               return null;
            }
            catch (Throwable x)
            {
               unwrapThrowable(x, declared);
            }
         }
         else if (Utils.isAttributeGetter(method))
         {
            String n = method.getName();
            String name = null;
            if (n.startsWith("is"))
               name = n.substring(2);
            else
               name = n.substring(3);

            try
            {
               return getAttribute(name);
            }
            catch (Throwable x)
            {
               unwrapThrowable(x, declared);
            }
         }
         else
         {
            Class[] parameters = method.getParameterTypes();
            String[] params = new String[parameters.length];
            for (int i = 0; i < parameters.length; ++i)
            {
               params[i] = parameters[i].getName();
            }

            try
            {
               return invokeOperation(method.getName(), args, params);
            }
            catch (Throwable x)
            {
               unwrapThrowable(x, declared);
            }
         }

         return null;
      }

      /**
       * Called to invoke setAttribute on a (possibly remote) MBeanServer.
       */
      protected abstract void setAttribute(Attribute attribute) throws Exception;

      /**
       * Called to invoke getAttribute on a (possibly remote) MBeanServer.
       */
      protected abstract Object getAttribute(String attribute) throws Exception;

      /**
       * Called to invoke invoke on a (possibly remote) MBeanServer.
       */
      protected abstract Object invokeOperation(String method, Object[] args, String[] params) throws Exception;

      /**
       * Rethrows as is the given throwable, if it is an instance of one of the given <code>declared</code> classes,
       * ot tries to (recursively) unwrap it and rethrow it.
       */
      protected void unwrapThrowable(Throwable x, Class[] declared) throws Throwable
      {
         if (declared != null)
         {
            // See if the exception is declared by the method
            // If so, just rethrow it.
            for (int i = 0; i < declared.length; ++i)
            {
               Class exception = declared[i];
               if (exception.isInstance(x)) throw x;
            }
         }

         // The exception is not declared, try to unwrap it
         if (x instanceof MBeanException)
         {
            unwrapThrowable(((MBeanException)x).getTargetException(), declared);
         }
         else if (x instanceof ReflectionException)
         {
            unwrapThrowable(((ReflectionException)x).getTargetException(), declared);
         }
         else if (x instanceof RuntimeOperationsException)
         {
            unwrapThrowable(((RuntimeOperationsException)x).getTargetException(), declared);
         }
         else if (x instanceof RuntimeMBeanException)
         {
            unwrapThrowable(((RuntimeMBeanException)x).getTargetException(), declared);
         }
         else if (x instanceof RuntimeErrorException)
         {
            unwrapThrowable(((RuntimeErrorException)x).getTargetError(), declared);
         }
         else
         {
            // Rethrow as is. Since this exception is not declared by the methods of the interface,
            // if it is checked will be throws as UndclaredThrowableException, if it is unchecked
            // will be rethrown as is.
            throw x;
         }
      }
   }

   private static class LocalHandler extends Handler
   {
      private MBeanServer m_server;
      private ObjectName m_name;

      private LocalHandler(MBeanServer server, ObjectName name)
      {
         m_server = server;
         m_name = name;
      }

      protected void setAttribute(Attribute attribute) throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException
      {
         m_server.setAttribute(m_name, attribute);
      }

      protected Object getAttribute(String attribute) throws InstanceNotFoundException, AttributeNotFoundException, MBeanException, ReflectionException
      {
         return m_server.getAttribute(m_name, attribute);
      }

      protected Object invokeOperation(String method, Object[] args, String[] params) throws InstanceNotFoundException, MBeanException, ReflectionException
      {
         return m_server.invoke(m_name, method, args, params);
      }
   }
}
