/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.servicemix.jbi.management;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.jbi.JBIException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.servicemix.jbi.container.EnvironmentContext;
import org.apache.servicemix.jbi.container.JBIContainer;
import org.apache.servicemix.jbi.framework.ComponentMBeanImpl;

/**
 * Management Context applied to a ServiceMix container
 * 
 * @version $Revision: 564607 $
 */
public class ManagementContext extends BaseSystemService implements ManagementContextMBean {
    /**
     * Default servicemix domain
     */
    public static final String DEFAULT_DOMAIN = "org.apache.servicemix";

    public static final String DEFAULT_CONNECTOR_PATH = "/jmxrmi";

    public static final int DEFAULT_CONNECTOR_PORT = 1099;

    private static final Log LOG = LogFactory.getLog(ManagementContext.class);

    protected Map<String, ObjectName> systemServices = new ConcurrentHashMap<String, ObjectName>();

    private Map<ObjectName, Object> beanMap = new ConcurrentHashMap<ObjectName, Object>();

    private MBeanServerContext mbeanServerContext = new MBeanServerContext();

    private ExecutorService executors;

    /**
     * Default Constructor
     */
    public ManagementContext() {
        mbeanServerContext.setJmxDomainName(DEFAULT_DOMAIN);
    }

    /**
     * Get the Description of the item
     * 
     * @return the description
     */
    public String getDescription() {
        return "JMX Management";
    }

    /**
     * Get the MBeanServer
     * 
     * @return the MBeanServer
     */
    public MBeanServer getMBeanServer() {
        return mbeanServerContext.getMBeanServer();
    }

    /**
     * @return the domain
     */
    public String getJmxDomainName() {
        return mbeanServerContext.getJmxDomainName();
    }

    /**
     * @return Returns the useMBeanServer.
     */
    public boolean isUseMBeanServer() {
        return mbeanServerContext.isUseMBeanServer();
    }

    /**
     * @param useMBeanServer
     *            The useMBeanServer to set.
     */
    public void setUseMBeanServer(boolean useMBeanServer) {
        mbeanServerContext.setUseMBeanServer(useMBeanServer);
    }

    /**
     * @return Returns the createMBeanServer flag.
     */
    public boolean isCreateMBeanServer() {
        return mbeanServerContext.isCreateMBeanServer();
    }

    /**
     * @param enableJMX
     *            Set createMBeanServer.
     */
    public void setCreateMBeanServer(boolean enableJMX) {
        mbeanServerContext.setCreateMBeanServer(enableJMX);
    }

    public void setNamingPort(int portNum) {
        mbeanServerContext.setConnectorPort(portNum);
    }

    public int getNamingPort() {
        return mbeanServerContext.getConnectorPort();
    }

    public boolean isCreateJmxConnector() {
        return mbeanServerContext.isCreateConnector();
    }

    public void setCreateJmxConnector(boolean createJmxConnector) {
        mbeanServerContext.setCreateConnector(createJmxConnector);
    }

    /**
     * Initialize the ManagementContext
     * 
     * @param container
     * @param server
     * @throws JBIException
     * 
     */
    public void init(JBIContainer container, MBeanServer server) throws JBIException {
        if (container.isEmbedded() && server == null) {
            mbeanServerContext.setUseMBeanServer(false);
            mbeanServerContext.setCreateMBeanServer(false);
        }
        mbeanServerContext.setMBeanServer(server);
        try {
            mbeanServerContext.start();
        } catch (IOException e) {
            LOG.error("Failed to start mbeanServerContext", e);
        }
        this.executors = Executors.newCachedThreadPool();
        super.init(container);
    }

    protected Class<ManagementContextMBean> getServiceMBean() {
        return ManagementContextMBean.class;
    }

    /**
     * Start the item.
     * 
     * @exception JBIException
     *                if the item fails to start.
     */
    public void start() throws JBIException {
        super.start();
    }

    /**
     * Stop the item. This suspends current messaging activities.
     * 
     * @exception JBIException
     *                if the item fails to stop.
     */
    public void stop() throws JBIException {
        super.stop();
    }

    /**
     * Shut down the item. The releases resources, preparatory to
     * uninstallation.
     * 
     * @exception JBIException
     *                if the item fails to shut down.
     */
    public void shutDown() throws JBIException {
        super.shutDown();
        // Unregister all mbeans
        ObjectName[] beans = beanMap.keySet().toArray(new ObjectName[beanMap.size()]);
        for (int i = 0; i < beans.length; i++) {
            try {
                unregisterMBean(beans[i]);
            } catch (Exception e) {
                LOG.debug("Could not unregister mbean", e);
            }
        }
        try {
            mbeanServerContext.stop();
        } catch (IOException e) {
            LOG.debug("Failed to shutdown mbeanServerContext cleanly", e);
        }
        executors.shutdown();
    }

    /**
     * Get a list of all binding components currently installed.
     * 
     * @return array of JMX object names of all installed BCs.
     */
    public ObjectName[] getBindingComponents() {
        return container.getRegistry().getBindingComponents();
    }

    /**
     * Lookup a JBI Installable Component by its unique name.
     * 
     * @param componentName -
     *            is the name of the BC or SE.
     * @return the JMX object name of the component's LifeCycle MBean or null.
     */
    public ObjectName getComponentByName(String componentName) {
        ComponentMBeanImpl component = container.getRegistry().getComponent(componentName);
        return component != null ? component.getMBeanName() : null;
    }

    /**
     * Get a list of all engines currently installed.
     * 
     * @return array of JMX object names of all installed SEs.
     */
    public ObjectName[] getEngineComponents() {
        return container.getRegistry().getEngineComponents();
    }

    /**
     * @return an array of ObjectNames for all Pojo components
     */
    public ObjectName[] getPojoComponents() {
        return container.getRegistry().getPojoComponents();
    }

    /**
     * Return current version and other info about this JBI Framework.
     * 
     * @return info String
     */
    public String getSystemInfo() {
        return "ServiceMix JBI Container: version: " + EnvironmentContext.getVersion();
    }

    /**
     * Lookup a system service by name.
     * 
     * @param serviceName -
     *            is the name of the system service
     * @return the JMX object name of the service or null
     */
    public ObjectName getSystemService(String serviceName) {
        return systemServices.get(serviceName);
    }

    /**
     * Looks up all JBI Framework System Services currently installed.
     * 
     * @return array of JMX object names of system services
     */
    public ObjectName[] getSystemServices() {
        ObjectName[] result = null;
        Collection<ObjectName> col = systemServices.values();
        result = new ObjectName[col.size()];
        col.toArray(result);
        return result;
    }

    /**
     * Check if a given JBI Installable Component is a Binding Component.
     * 
     * @param componentName -
     *            the unique name of the component
     * @return true if the component is a binding
     */
    public boolean isBinding(String componentName) {
        ComponentMBeanImpl component = container.getRegistry().getComponent(componentName);
        return component != null ? component.isBinding() : false;
    }

    /**
     * Check if a given JBI Component is a service engine.
     * 
     * @param componentName -
     *            the unique name of the component
     * @return true if the component is a service engine
     */
    public boolean isEngine(String componentName) {
        ComponentMBeanImpl component = container.getRegistry().getComponent(componentName);
        return component != null ? component.isEngine() : false;
    }

    /**
     * Start a Component
     * 
     * @param componentName
     * @return the status
     * @throws JBIException
     */
    public String startComponent(String componentName) throws JBIException {
        String result = "NOT FOUND: " + componentName;
        ObjectName objName = getComponentByName(componentName);
        if (objName != null) {
            ComponentMBeanImpl mbean = (ComponentMBeanImpl) beanMap.get(objName);
            if (mbean != null) {
                mbean.start();
                result = mbean.getCurrentState();
            }
        }
        return result;
    }

    /**
     * Stop a Component
     * 
     * @param componentName
     * @return the status
     * @throws JBIException
     */
    public String stopComponent(String componentName) throws JBIException {
        String result = "NOT FOUND: " + componentName;
        ObjectName objName = getComponentByName(componentName);
        if (objName != null) {
            ComponentMBeanImpl mbean = (ComponentMBeanImpl) beanMap.get(objName);
            if (mbean != null) {
                mbean.stop();
                result = mbean.getCurrentState();
            }
        }
        return result;
    }

    /**
     * Shutdown a Component
     * 
     * @param componentName
     * @return the status
     * @throws JBIException
     */
    public String shutDownComponent(String componentName) throws JBIException {
        String result = "NOT FOUND: " + componentName;
        ObjectName objName = getComponentByName(componentName);
        if (objName != null) {
            ComponentMBeanImpl mbean = (ComponentMBeanImpl) beanMap.get(objName);
            if (mbean != null) {
                mbean.shutDown();
                result = mbean.getCurrentState();
            }
        }
        return result;
    }

    /**
     * Formulate and return the MBean ObjectName of a custom control MBean for a
     * JBI component.
     * 
     * @param type
     * @param name
     * @return the JMX ObjectName of the MBean, or <code>null</code> if
     *         <code>customName</code> is invalid.
     */
    public ObjectName createCustomComponentMBeanName(String type, String name) {
        Map<String, String> result = new LinkedHashMap<String, String>();
        result.put("ContainerName", container.getName());
        result.put("Type", "Component");
        result.put("Name", sanitizeString(name));
        result.put("SubType", sanitizeString(type));
        return createObjectName(result);
    }

    /**
     * Create an ObjectName
     * 
     * @param provider
     * @return the ObjectName
     */
    public ObjectName createObjectName(MBeanInfoProvider provider) {
        Map<String, String> props = createObjectNameProps(provider);
        return createObjectName(props);
    }

    /**
     * Create an ObjectName
     * 
     * @param name
     * 
     * @return the ObjectName
     */
    public ObjectName createObjectName(String name) {
        ObjectName result = null;
        try {
            result = new ObjectName(name);
        } catch (MalformedObjectNameException e) {
            // shouldn't happen
            String error = "Could not create ObjectName for " + name;
            LOG.error(error, e);
            throw new RuntimeException(error);
        }
        return result;
    }

    /**
     * Create an ObjectName
     * 
     * @param domain
     * 
     * @return the ObjectName
     */
    public ObjectName createObjectName(String domain, Map<String, String> props) {
        StringBuffer sb = new StringBuffer();
        sb.append(domain).append(':');
        int i = 0;
        for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry) it.next();
            if (i++ > 0) {
                sb.append(",");
            }
            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }
        ObjectName result = null;
        try {
            result = new ObjectName(sb.toString());
        } catch (MalformedObjectNameException e) {
            // shouldn't happen
            String error = "Could not create ObjectName for " + props;
            LOG.error(error, e);
            throw new RuntimeException(error);
        }
        return result;
    }

    /**
     * Create an ObjectName
     * 
     * @param props
     * @return the ObjectName
     */
    public ObjectName createObjectName(Map<String, String> props) {
        return createObjectName(getJmxDomainName(), props);
    }

    /**
     * Create a String used to create an ObjectName
     * 
     * @param provider
     * @return the ObjectName
     */
    public Map<String, String> createObjectNameProps(MBeanInfoProvider provider) {
        return createObjectNameProps(provider, false);
    }

    /**
     * Create a String used to create an ObjectName
     * 
     * @param provider
     * @return the ObjectName
     */
    public Map<String, String> createObjectNameProps(MBeanInfoProvider provider, boolean subTypeBeforeName) {
        Map<String, String> result = new LinkedHashMap<String, String>();
        result.put("ContainerName", container.getName());
        result.put("Type", sanitizeString(provider.getType()));
        if (subTypeBeforeName && provider.getSubType() != null) {
            result.put("SubType", sanitizeString(provider.getSubType()));
        }
        result.put("Name", sanitizeString(provider.getName()));
        if (!subTypeBeforeName && provider.getSubType() != null) {
            result.put("SubType", sanitizeString(provider.getSubType()));
        }
        return result;
    }

    /**
     * The ':' and '/' characters are reserved in ObjectNames
     * 
     * @param in
     * @return sanitized String
     */
    private static String sanitizeString(String in) {
        String result = null;
        if (in != null) {
            result = in.replace(':', '_');
            result = result.replace('/', '_');
            result = result.replace('\\', '_');
            result = result.replace('?', '_');
            result = result.replace('=', '_');
            result = result.replace(',', '_');
        }
        return result;
    }

    /**
     * Register an MBean
     * 
     * @param resource
     * @param name
     * @param interfaceMBean
     * @throws JMException
     */
    public void registerMBean(ObjectName name, MBeanInfoProvider resource, Class interfaceMBean) throws JMException {
        registerMBean(name, resource, interfaceMBean, resource.getDescription());
    }

    /**
     * Register an MBean
     * 
     * @param resource
     * @param name
     * @param interfaceMBean
     * @param description
     * @throws JMException
     */
    public void registerMBean(ObjectName name, Object resource, Class interfaceMBean, String description) throws JMException {
        if (mbeanServerContext.getMBeanServer() != null) {
            Object mbean = MBeanBuilder.buildStandardMBean(resource, interfaceMBean, description, executors);
            if (mbeanServerContext.getMBeanServer().isRegistered(name)) {
                mbeanServerContext.getMBeanServer().unregisterMBean(name);
            }
            mbeanServerContext.getMBeanServer().registerMBean(mbean, name);
            beanMap.put(name, resource);
        }
    }

    /**
     * Retrive an System ObjectName
     * 
     * @param domainName
     * @param containerName
     * @param interfaceType
     * @return the ObjectName
     */
    public static ObjectName getSystemObjectName(String domainName, String containerName, Class interfaceType) {
        String tmp = domainName + ":ContainerName=" + containerName + ",Type=SystemService,Name=" + getSystemServiceName(interfaceType);
        ObjectName result = null;
        try {
            result = new ObjectName(tmp);
        } catch (MalformedObjectNameException e) {
            LOG.error("Failed to build ObjectName:", e);
        } catch (NullPointerException e) {
            LOG.error("Failed to build ObjectName:", e);
        }
        return result;
    }

    public static String getSystemServiceName(Class interfaceType) {
        String name = interfaceType.getName();
        name = name.substring(name.lastIndexOf('.') + 1);
        if (name.endsWith("MBean")) {
            name = name.substring(0, name.length() - 5);
        }
        return name;
    }

    public static ObjectName getContainerObjectName(String domainName, String containerName) {
        String tmp = domainName + ":ContainerName=" + containerName + ",Type=JBIContainer";
        ObjectName result = null;
        try {
            result = new ObjectName(tmp);
        } catch (MalformedObjectNameException e) {
            LOG.debug("Unable to build ObjectName", e);
        } catch (NullPointerException e) {
            LOG.debug("Unable to build ObjectName", e);
        }
        return result;
    }

    /**
     * Register a System service
     * 
     * @param service
     * @param interfaceType
     * @throws JBIException
     */
    public void registerSystemService(BaseSystemService service, Class interfaceType) throws JBIException {
        try {

            String name = service.getName();
            if (systemServices.containsKey(name)) {
                throw new JBIException("A system service for the name " + name + " is already registered");
            }
            ObjectName objName = createObjectName(service);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Registering system service: " + objName);
            }
            registerMBean(objName, service, interfaceType, service.getDescription());
            systemServices.put(name, objName);
        } catch (MalformedObjectNameException e) {
            throw new JBIException(e);
        } catch (JMException e) {
            throw new JBIException(e);
        }
    }

    /**
     * Unregister a System service
     * 
     * @param service
     * @throws JBIException
     */
    public void unregisterSystemService(BaseSystemService service) throws JBIException {
        String name = service.getName();
        if (!systemServices.containsKey(name)) {
            throw new JBIException("A system service for the name " + name + " is not registered");
        }
        ObjectName objName = systemServices.remove(name);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Unregistering system service: " + objName);
        }
        unregisterMBean(objName);
    }

    /**
     * Unregister an MBean
     * 
     * @param name
     * @throws JBIException
     */
    public void unregisterMBean(ObjectName name) throws JBIException {
        try {
            mbeanServerContext.unregisterMBean(name);
            beanMap.remove(name);
        } catch (JMException e) {
            LOG.error("Failed to unregister mbean: " + name, e);
            throw new JBIException(e);
        }
    }

    /**
     * Unregister an MBean
     * 
     * @param bean
     * @throws JBIException
     */
    public void unregisterMBean(Object bean) throws JBIException {
        for (Iterator i = beanMap.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry) i.next();
            if (entry.getValue() == bean) {
                ObjectName name = (ObjectName) entry.getKey();
                unregisterMBean(name);
                break;
            }
        }
    }

    /**
     * Get an array of MBeanOperationInfo
     * 
     * @return array of OperationInfos
     * @throws JMException
     */
    public MBeanAttributeInfo[] getAttributeInfos() throws JMException {
        AttributeInfoHelper helper = new AttributeInfoHelper();
        helper.addAttribute(getObjectToManage(), "bindingComponents", "Get list of all binding components");
        helper.addAttribute(getObjectToManage(), "engineComponents", "Get list of all engine components");
        helper.addAttribute(getObjectToManage(), "pojoComponents", "Get list of all pojo components");
        helper.addAttribute(getObjectToManage(), "systemInfo", "Return current version");
        helper.addAttribute(getObjectToManage(), "systemServices", "Get list of system services");
        return AttributeInfoHelper.join(super.getAttributeInfos(), helper.getAttributeInfos());
    }

    public MBeanOperationInfo[] getOperationInfos() throws JMException {
        OperationInfoHelper helper = new OperationInfoHelper();
        ParameterHelper ph = helper.addOperation(getObjectToManage(), "getComponentByName", 1, "look up Component by name");
        ph.setDescription(0, "name", "Component name");
        ph = helper.addOperation(getObjectToManage(), "getSystemService", 1, "look up System service by name");
        ph.setDescription(0, "name", "System name");
        ph = helper.addOperation(getObjectToManage(), "isBinding", 1, "Is Component a binding Component?");
        ph.setDescription(0, "name", "Component name");
        ph = helper.addOperation(getObjectToManage(), "isEngine", 1, "Is Component a service engine?");
        ph.setDescription(0, "name", "Component name");
        return OperationInfoHelper.join(super.getOperationInfos(), helper.getOperationInfos());
    }

}
