/**
 * Copyright (c) 2007-2012 EBM WebSourcing, 2012-2015 Linagora
 * 
 * This program/library 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 program/library 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 program/library; If not, see <http://www.gnu.org/licenses/>
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.microkernel.server;

import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Set;
import java.util.Vector;

import javax.jbi.management.AdminServiceMBean;
import javax.management.Descriptor;
import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.RuntimeOperationsException;
import javax.management.modelmbean.DescriptorSupport;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import javax.management.modelmbean.RequiredModelMBean;

import org.objectweb.fractal.adl.ADLException;
import org.objectweb.fractal.api.Component;
import org.objectweb.fractal.api.NoSuchInterfaceException;
import org.objectweb.fractal.api.control.ContentController;
import org.objectweb.fractal.jmx.agent.Introspector;
import org.objectweb.fractal.util.Fractal;
import org.ow2.petals.jmx.api.api.AdminServiceClient;
import org.ow2.petals.jmx.api.api.DeploymentServiceClient;
import org.ow2.petals.jmx.api.api.EndpointRegistryClient;
import org.ow2.petals.jmx.api.api.InstallationServiceClient;
import org.ow2.petals.jmx.api.api.LoggerServiceClient;
import org.ow2.petals.jmx.api.api.PetalsAdminServiceClient;
import org.ow2.petals.jmx.api.api.RouterMonitorServiceClient;
import org.ow2.petals.jmx.api.api.TopologyServiceClient;
import org.ow2.petals.jmx.api.api.monitoring.LocalTransporterMonitoringServiceClient;
import org.ow2.petals.microkernel.api.admin.PetalsAdminServiceMBean;
import org.ow2.petals.microkernel.api.communication.JMXService;
import org.ow2.petals.microkernel.api.jbi.management.DeploymentServiceMBean;
import org.ow2.petals.microkernel.api.jbi.management.InstallationServiceMBean;
import org.ow2.petals.microkernel.api.jbi.messaging.registry.EndpointRegistryMBean;
import org.ow2.petals.microkernel.api.server.FractalHelper;
import org.ow2.petals.microkernel.jbi.messaging.routing.monitoring.RouterMonitorMBean;
import org.ow2.petals.microkernel.system.logging.LoggingServiceMBean;

/**
 * This class helps the Launcher to register MBean.
 * @author Adrien Louis - EBM WebSourcing
 */
public class MBeanHelper {

    public static String CURRENCY_TIME_LIMIT = "-1"; // !!!

    public static final String DOMAIN = "Petals";

    /**
     * Find the local JMX server in the petals component
     * 
     * @param petalsComposite
     *            the petals component
     * @return the local jmx server
     * @throws NoSuchInterfaceException
     * @throws ADLException
     */
    public static final MBeanServer findLocalJMXServer(Component petalsComposite)
            throws NoSuchInterfaceException, ADLException {
        ContentController petalsContentController = Fractal.getContentController(petalsComposite);
        Component jmxService = FractalHelper.getRecursiveComponentByName(petalsContentController,
                FractalHelper.JMX_COMPONENT);

        MBeanServer jmxServer = ((JMXService) jmxService.getFcInterface("service"))
                .getLocalJMXServer();

        return jmxServer;
    }

    public static final void registerMBeans(Component petalsComposite) throws Exception {
        ContentController contentController = Fractal.getContentController(petalsComposite);

        // unactivate the method invokation cache for the Fractal MBeans
        Introspector.CURRENCY_TIME_LIMIT = "-1";

        MBeanServer jmxServer = findLocalJMXServer(petalsComposite);

        // register AdminService
        registerComponent(contentController, FractalHelper.ADMIN_COMPONENT, jmxServer,
                AdminServiceMBean.class, DOMAIN + ":name="
                        + AdminServiceClient.ADMIN_SERVICE_MBEAN_NAME + ",type=service");

        // register DeploymentService
        registerComponent(contentController, FractalHelper.DEPLOYMENT_COMPONENT, jmxServer,
                DeploymentServiceMBean.class, DOMAIN + ":name="
                        + DeploymentServiceClient.DEPLOYMENT_SERVICE_MBEAN_NAME
                        + ",type=service");

        // register InstallationService
        registerComponent(contentController, FractalHelper.INSTALLATION_COMPONENT, jmxServer,
                InstallationServiceMBean.class, DOMAIN + ":name="
                        + InstallationServiceClient.INSTALLATION_SERVICE_MBEAN_NAME
                        + ",type=service");

        // register EndpointRegistry
        registerComponent(contentController, FractalHelper.ENDPOINT_COMPONENT, jmxServer,
                EndpointRegistryMBean.class, DOMAIN + ":name="
                        + EndpointRegistryClient.ENDPOINT_REGISTRY_MBEAN_NAME + ",type=service");

        // register Petals Admin
        registerComponent(contentController, FractalHelper.PETALSADMIN_COMPONENT, jmxServer,
                PetalsAdminServiceMBean.class, DOMAIN + ":name="
                        + PetalsAdminServiceClient.PETALS_ADMIN_MBEAN_NAME + ",type=service");
        
        // register Router Monitor
        registerComponent(contentController, FractalHelper.ROUTER_MONITOR_COMPONENT, jmxServer,
                RouterMonitorMBean.class, DOMAIN + ":name="
                        + RouterMonitorServiceClient.ROUTER_MONITOR_MBEAN_NAME + ",type=service");

        // register Petals TransporterLocalMonitoring
        registerSimplyComponent(contentController,
                FractalHelper.LOCAL_TRANSPORTER_MONITORING_COMPONENT, jmxServer, DOMAIN + ":name="
                        + LocalTransporterMonitoringServiceClient.LOCAL_TRANSPORTER_MONITORING_MBEAN_NAME
                        + ",type=service");
        // register Logger service
        registerComponent(contentController, FractalHelper.LOGGER_COMPONENT, jmxServer,
                LoggingServiceMBean.class, DOMAIN + ":name="
                        + LoggerServiceClient.LOGGER_SERVICE_MBEAN_NAME + ",type=service");

        // register Topology service
        registerComponent(contentController, FractalHelper.TOPOLOGY_COMPONENT, jmxServer,
                LoggingServiceMBean.class, DOMAIN + ":name="
                        + TopologyServiceClient.TOPOLOGY_SERVICE_MBEAN_NAME + ",type=service");
    }

    /**
     * Return the MBean name from the jmx server with the specified pattern
     * 
     * @param serviceName
     *            The name of the service to retrieve, must be {@code null}
     * @param jmxServer
     *            The JMX server instance, must be non {@code null}
     * 
     * @return the objectname, null if no object match
     * @throws MalformedObjectNameException
     */
    @SuppressWarnings("unchecked")
    public static final ObjectName retrieveServiceMBean(String serviceName, MBeanServer jmxServer)
            throws MalformedObjectNameException {

        ObjectName result = null;

        // crate the search filter
        Hashtable<String, String> attributes = new Hashtable<String, String>();
        attributes.put("name", serviceName);
        attributes.put("type", "service");
        ObjectName objName = new ObjectName("Petals", attributes);

        Set objNames = jmxServer.queryNames(objName, null);
        if ((objNames != null) && (objNames.size() == 1)) {
            result = (ObjectName) objNames.iterator().next();
        }
        return result;
    }

    /**
     * Register a fractal component-service to the jmx server. Assume that the
     * interface name of the fractal component is "service"
     * 
     * @param contentController
     *            contentcontroller where the fractal comp can be found
     * @param fractalName
     *            the name of the fractal comp
     * @param jmxServer
     *            the jmx server to register to
     * @param mbeanClass
     *            the interface of the mbean
     * @param mbeanName
     *            the name of the mbean
     * @throws Exception
     */
    public static final void registerComponent(ContentController contentController,
            String fractalName, MBeanServer jmxServer, Class mbeanClass, String mbeanName)
            throws Exception {
        Component fractalComponent = FractalHelper.getRecursiveComponentByName(contentController,
                fractalName);
        Object object = fractalComponent.getFcInterface("/content");

        jmxServer.registerMBean(createCustomMBean(object, mbeanClass), new ObjectName(mbeanName));
    }

    /**
     * Register a fractal component-service to the jmx server, as is.
     * 
     * @param contentController
     *            contentcontroller where the fractal comp can be found
     * @param fractalName
     *            the name of the fractal comp
     * @param jmxServer
     *            the jmx server to register to
     * @param mbeanName
     *            the name of the mbean
     * @throws Exception
     */
    public static final void registerSimplyComponent(ContentController contentController,
            String fractalName, MBeanServer jmxServer, String mbeanName) throws Exception {
        Component fractalComponent = FractalHelper.getRecursiveComponentByName(contentController,
                fractalName);
        Object object = fractalComponent.getFcInterface("/content");

        jmxServer.registerMBean(object, new ObjectName(mbeanName));
    }

    /**
     * Register a fractal component-service to the jmx server. Assume that the
     * interface name of the fractal component is "service"
     * 
     * @param contentController
     *            contentcontroller where the fractal comp can be found
     * @param fractalName
     *            the name of the fractal comp
     * @param jmxServer
     *            the jmx server to register to
     * @param mbeanClass
     *            the interface of the mbean
     * @param mbeanName
     *            the name of the mbean
     * @throws Exception
     */
    public static final void registerMBean(ContentController contentController, String fractalName,
            MBeanServer jmxServer, Class mbeanClass, String mbeanName) throws Exception {
        Component fractalComponent = FractalHelper.getRecursiveComponentByName(contentController,
                fractalName);
        Object object = fractalComponent.getFcInterface("/content");

        jmxServer.registerMBean(createCustomMBean(object, mbeanClass), new ObjectName(mbeanName));
    }

    /**
     * Create a custom MBean which process the method isXXX, setXXX, or getXXX
     * as getter only if an attribute is define in the class.
     * 
     * @param object
     * @param mbeanClass
     * @return
     * @throws RuntimeOperationsException
     * @throws MBeanException
     * @throws InstanceNotFoundException
     * @throws InvalidTargetObjectTypeException
     */
    public static final RequiredModelMBean createCustomMBean(Object object, Class<?> mbeanClass)
            throws RuntimeOperationsException, MBeanException, InstanceNotFoundException,
            InvalidTargetObjectTypeException {
        Method[] methods = mbeanClass.getMethods();
        Vector<ModelMBeanAttributeInfo> attributes = new Vector<ModelMBeanAttributeInfo>();
        Vector<ModelMBeanOperationInfo> operators = new Vector<ModelMBeanOperationInfo>();

        for (int i = 0; i < methods.length; i++) {
            // IF THE METHOD STARTS WITH "is," "get," or "set" THEN TRY TO ADD
            // AN ATTRIBUTE TO THE MODELMBEAN
            if ((methods[i].getName().startsWith("get"))
                    || (methods[i].getName().startsWith("set"))
                    || (methods[i].getName().startsWith("is"))) {
                String fieldName;
                if (methods[i].getName().startsWith("is")) {
                    fieldName = methods[i].getName().substring(2);
                } else {
                    fieldName = methods[i].getName().substring(3);
                }
                fieldName = fieldName.substring(0, 1).toLowerCase().concat(fieldName.substring(1));
                try {
                    try {
                        object.getClass().getDeclaredField(fieldName);
                        ModelMBeanAttributeInfo mmai = doAttribute(methods, i);
                        if (mmai != null) {
                            attributes.add(mmai);
                        }
                    } catch (NoSuchFieldException e) {
                        // ADD THE METHOD AS AN OPERATION TO THE MODEL MBEAN
                        ModelMBeanOperationInfo mmoi = doOperation(methods, i, true);
                        if (mmoi != null) {
                            operators.add(mmoi);
                        }
                    }
                } catch (Exception e) {
                    // not possible
                }
            } else {
                // ADD THE METHOD AS AN OPERATION TO THE MODEL MBEAN
                try {
                    ModelMBeanOperationInfo mmoi = doOperation(methods, i, false);
                    if (mmoi != null) {
                        operators.add(mmoi);
                    }
                } catch (Exception e) {
                    // not possible
                }
            }
        }

        // ATTRIBUTES
        ModelMBeanAttributeInfo[] attributeArray = new ModelMBeanAttributeInfo[attributes.size()];
        attributes.copyInto(attributeArray);

        // METHODS
        ModelMBeanOperationInfo[] operatorArray = new ModelMBeanOperationInfo[operators.size()];
        operators.copyInto(operatorArray);

        // Descriptor
        Descriptor desc = new DescriptorSupport();
        desc.setField("name", object.getClass().getName());
        desc.setField("descriptorType", "MBean");

        // CREATE MODELMBEAN
        ModelMBeanInfo mminfo = new ModelMBeanInfoSupport(object.getClass().getName(), object
                .getClass().getName(), attributeArray, new ModelMBeanConstructorInfo[0],
                operatorArray, new ModelMBeanNotificationInfo[0], desc);
        RequiredModelMBean model = new RequiredModelMBean(mminfo);
        model.setManagedResource(object, "ObjectReference");
        return model;
    }

    /**
     * Build the ModelMBeanAttributeInfo for the method. Take the array of the
     * objects methods and the index of the method to work on. It needs the
     * entire array of methods to determine matching sets of setters and
     * getters.
     * 
     * @param methods
     *            the array of the objects methods to work on.
     * @param i
     *            the index of the method to work on.
     * @return the ModelMBeanOperationInfo for the method.
     * @throws JMException
     *             if something goes wrong.
     */
    private static ModelMBeanAttributeInfo doAttribute(Method methods[], int i) throws JMException {
        String name = null;
        Method getter = null;
        Method setter = null;
        String type = null;

        // THIS METHOD IS A GETTER
        if ((methods[i].getName().startsWith("get"))
                && (methods[i].getParameterTypes().length == 0)) {
            String setName = "s" + methods[i].getName().substring(1);
            name = methods[i].getName().substring(3);
            getter = methods[i];
            type = methods[i].getReturnType().getName();
            // CHECK TO SEE IF THERE IS THE EQUVALENT WRITE OBJECT
            for (int i_ = 0; i_ < methods.length; i_++) {
                if (methods[i_].getName().equals(setName)) {
                    setter = methods[i_];
                    break;
                }
            }

        } // THIS OBJECT IS A SETTER
        else if ((methods[i].getName().startsWith("set"))
                && (methods[i].getParameterTypes().length == 1)) {
            String getName = "g" + methods[i].getName().substring(1);
            name = methods[i].getName().substring(3);
            // CHECK TO SEE IF THERE IS THE EQUVALENT READ OBJECT
            // IF SO, THEN THE GET EQUIVALENT WOULD HAVE RETURNED
            // A ModelMBeanAttributeInfo, SO RETURN A NULL
            // SO THIS ATTRIBUTE DOESN'T SHOW UP TWICE
            for (int i_ = 0; i_ < methods.length; i_++) {
                if (methods[i_].getName().equals(getName)) {
                    return null;
                }
            }
            setter = methods[i];
            type = methods[i].getParameterTypes()[0].getName();

        } // THIS OBJECT IS A GETTER WITH NO SETTER
        else if ((methods[i].getName().startsWith("is"))
                && (methods[i].getParameterTypes() == null)) {
            name = methods[i].getName().substring(2);
            getter = methods[i];
            type = boolean.class.getName();
        } // NOT A COMPLIENT get, set, OR is ATTRIBUTE
        else {
            return null;
        }

        Descriptor desc = new DescriptorSupport();
        desc.setField("name", name); // THE NAME THAT THE ATTRIBUTE WILL HAVE
        desc.setField("descriptorType", "attribute"); // THIS IS MANDATORY FOR
        // ANY ATTRIBUTES
        // DESCRIPTION
        desc.setField("displayName", name);
        // MAKE SURE THAT THE AGENT DOESN'T READ FROM CACHE (0 does not work
        // with JMX RI)
        desc.setField("currencyTimeLimit", CURRENCY_TIME_LIMIT);

        if (getter != null) {
            desc.setField("getMethod", getter.getName());
        }
        if (setter != null) {
            desc.setField("setMethod", setter.getName());
        }

        ModelMBeanAttributeInfo mmai = new ModelMBeanAttributeInfo(name, type, name,
                (getter != null), (setter != null), false, desc);

        return mmai;
    }

    /*
     * Build the ModelMBeanOperationInfo for the method. Take the array of the
     * objects methods and the index of the method to work on.
     * 
     * @param methods the array of the objects methods to work on.
     * 
     * @param i the index of the method to work on.
     * 
     * @return the ModelMBeanOperationInfo for the method.
     * 
     * @throws JMException if something goes wrong.
     */
    private static ModelMBeanOperationInfo doOperation(Method methods[], int i,
            boolean isGetterOrSetter) throws JMException {
        Descriptor desc = new DescriptorSupport();
        desc.setField("name", methods[i].getName());
        desc.setField("descriptorType", "operation");
        // MAKE SURE THAT THE AGENT DOESN'T READ FROM CACHE (0 does not work
        // with JMX RI)
        // desc.setField("currencyTimeLimit", "1"); //!!!
        desc.setField("currencyTimeLimit", CURRENCY_TIME_LIMIT);

        // IF THE METHOD STARTS WITH "get" or "is" TREAT IT AS A GETTER.
        // IF IT STARTS WITH A "set" TREAT IT AS A SETTER.
        if (isGetterOrSetter) {
            if ((methods[i].getName().startsWith("get") || methods[i].getName().startsWith("is"))
                    && (methods[i].getParameterTypes().length == 0)) {
                desc.setField("role", "getter");
            } else if (methods[i].getName().startsWith("set")
                    && (methods[i].getParameterTypes().length == 1)) {
                desc.setField("role", "setter");
            }
        } else {
            desc.setField("role", "operation");
        }        

        ModelMBeanOperationInfo mmoi = new ModelMBeanOperationInfo(methods[i].getName(),
                methods[i], desc);
        return mmoi;
    }

}
