/**
 * 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.camel.management;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.RequiredModelMBean;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

import org.apache.camel.impl.ServiceSupport;
import org.apache.camel.spi.InstrumentationAgent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler;

/**
 * Default implementation of the Camel JMX service agent
 */
public class DefaultInstrumentationAgent extends ServiceSupport implements InstrumentationAgent {

    public static final String DEFAULT_DOMAIN = "org.apache.camel";
    public static final String DEFAULT_HOST = "localhost";
    public static final int DEFAULT_REGISTRY_PORT = 1099;
    public static final int DEFAULT_CONNECTION_PORT = -1;
    public static final String DEFAULT_SERVICE_URL_PATH = "/jmxrmi/camel";
    private static final transient Log LOG = LogFactory.getLog(DefaultInstrumentationAgent.class);

    private MBeanServer server;
    private Set<ObjectName> mbeans = new HashSet<ObjectName>();
    private MetadataMBeanInfoAssembler assembler;
    private JMXConnectorServer cs;

    private Integer registryPort;
    private Integer connectorPort;
    private String mBeanServerDefaultDomain;
    private String mBeanObjectDomainName;
    private String serviceUrlPath;
    private Boolean usePlatformMBeanServer = true;
    private Boolean createConnector;

    protected void finalizeSettings() {
        if (registryPort == null) {
            registryPort = Integer.getInteger(JmxSystemPropertyKeys.REGISTRY_PORT,
                    DEFAULT_REGISTRY_PORT);
        }

        if (connectorPort == null) {
            connectorPort = Integer.getInteger(JmxSystemPropertyKeys.CONNECTOR_PORT,
                    DEFAULT_CONNECTION_PORT);
        }

        if (mBeanServerDefaultDomain == null) {
            mBeanServerDefaultDomain =
                System.getProperty(JmxSystemPropertyKeys.DOMAIN, DEFAULT_DOMAIN);
        }

        if (mBeanObjectDomainName == null) {
            mBeanObjectDomainName =
                System.getProperty(JmxSystemPropertyKeys.MBEAN_DOMAIN, DEFAULT_DOMAIN);
        }

        if (serviceUrlPath == null) {
            serviceUrlPath =
                System.getProperty(JmxSystemPropertyKeys.SERVICE_URL_PATH,
                        DEFAULT_SERVICE_URL_PATH);
        }

        if (createConnector == null) {
            createConnector = Boolean.getBoolean(JmxSystemPropertyKeys.CREATE_CONNECTOR);
        }

        // "Use platform mbean server" is true by default
        if (System.getProperty(JmxSystemPropertyKeys.USE_PLATFORM_MBS) != null) {
            usePlatformMBeanServer = Boolean.getBoolean(JmxSystemPropertyKeys.USE_PLATFORM_MBS);
        }
    }


    public void setRegistryPort(Integer value) {
        registryPort = value;
    }

    public void setConnectorPort(Integer value) {
        connectorPort = value;
    }

    public void setMBeanServerDefaultDomain(String value) {
        mBeanServerDefaultDomain = value;
    }

    public void setMBeanObjectDomainName(String value) {
        mBeanObjectDomainName = value;
    }

    public void setServiceUrlPath(String value) {
        serviceUrlPath = value;
    }

    public void setCreateConnector(Boolean flag) {
        createConnector = flag;
    }

    public void setUsePlatformMBeanServer(Boolean flag) {
        usePlatformMBeanServer = flag;
    }

    public MBeanServer getMBeanServer() {
        return server;
    }

    public void register(Object obj, ObjectName name) throws JMException {
        register(obj, name, false);
    }

    public void register(Object obj, ObjectName name, boolean forceRegistration) throws JMException {
        try {
            registerMBeanWithServer(obj, name, forceRegistration);
        } catch (NotCompliantMBeanException e) {
            // If this is not a "normal" MBean, then try to deploy it using JMX
            // annotations
            ModelMBeanInfo mbi = null;
            mbi = assembler.getMBeanInfo(obj, name.toString());
            RequiredModelMBean mbean = (RequiredModelMBean)server.instantiate(RequiredModelMBean.class
                .getName());
            mbean.setModelMBeanInfo(mbi);
            try {
                mbean.setManagedResource(obj, "ObjectReference");
            } catch (InvalidTargetObjectTypeException itotex) {
                throw new JMException(itotex.getMessage());
            }
            registerMBeanWithServer(mbean, name, forceRegistration);
        }
    }

    public void unregister(ObjectName name) throws JMException {
        server.unregisterMBean(name);
    }

    protected void doStart() throws Exception {
        assembler = new MetadataMBeanInfoAssembler();
        assembler.setAttributeSource(new AnnotationJmxAttributeSource());

        // create mbean server if is has not be injected.
        if (server == null) {
            finalizeSettings();
            createMBeanServer();
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Starting JMX agent on server: " + getMBeanServer());
        }
    }

    protected void doStop() throws Exception {
        // close JMX Connector
        if (cs != null) {
            try {
                cs.stop();
            } catch (IOException e) {
                // ignore
            }
            cs = null;
        }

        // Using the array to hold the busMBeans to avoid the
        // CurrentModificationException
        Object[] mBeans = mbeans.toArray();
        int caught = 0;
        for (Object name : mBeans) {
            mbeans.remove((ObjectName)name);
            try {
                unregister((ObjectName)name);
            } catch (JMException jmex) {
                LOG.info("Exception unregistering MBean", jmex);
                caught++;
            }
        }
        if (caught > 0) {
            LOG.warn("A number of " + caught
                     + " exceptions caught while unregistering MBeans during stop operation."
                     + " See INFO log for details.");
        }
    }

    private void registerMBeanWithServer(Object obj, ObjectName name, boolean forceRegistration)
        throws JMException {

        // have we already registered the bean, there can be shared instances in the camel routes
        boolean exists = server.isRegistered(name);
        if (exists) {
            if (forceRegistration) {
                LOG.info("ForceRegistration enabled, unregistering existing MBean");
                server.unregisterMBean(name);
            } else {
                // okay ignore we do not want to force it and it could be a shared instance
                if (LOG.isDebugEnabled()) {
                    LOG.debug("MBean already registered with objectname: " + name);
                }
            }
        }

        // register bean if by force or not exsists
        ObjectInstance instance = null;
        if (forceRegistration || !exists) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Registering MBean with objectname: " + name);
            }
            instance = server.registerMBean(obj, name);
        }

        if (instance != null) {
            ObjectName registeredName = instance.getObjectName();
            if (LOG.isDebugEnabled()) {
                LOG.debug("Registered MBean with objectname: " + registeredName);
            }
            
            mbeans.add(registeredName);
        }
    }

    protected void createMBeanServer() {
        String hostName = DEFAULT_HOST;
        boolean canAccessSystemProps = true;
        try {
            // we'll do it this way mostly to determine if we should lookup the
            // hostName
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                sm.checkPropertiesAccess();
            }
        } catch (SecurityException se) {
            canAccessSystemProps = false;
        }

        if (canAccessSystemProps) {
            try {
                hostName = InetAddress.getLocalHost().getHostName();
            } catch (UnknownHostException uhe) {
                LOG.info("Cannot determine localhost name. Using default: "
                         + DEFAULT_REGISTRY_PORT, uhe);
                hostName = DEFAULT_HOST;
            }
        } else {
            hostName = DEFAULT_HOST;
        }

        server = findOrCreateMBeanServer();

        try {
            // Create the connector if we need
            if (createConnector) {
                createJmxConnector(hostName);
            }
        } catch (IOException ioe) {
            LOG.warn("Could not create and start JMX connector.", ioe);
        }
    }

    @SuppressWarnings("unchecked")
    protected MBeanServer findOrCreateMBeanServer() {

        // return platform mbean server if the option is specified.
        if (usePlatformMBeanServer) {
            return ManagementFactory.getPlatformMBeanServer();
        }

        // look for the first mbean server that has match default domain name
        List<MBeanServer> servers =
            (List<MBeanServer>)MBeanServerFactory.findMBeanServer(null);

        for (MBeanServer server : servers) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Found MBeanServer with default domain " + server.getDefaultDomain());
            }
            
            if (mBeanServerDefaultDomain.equals(server.getDefaultDomain())) {
                return server;
            }
        }

        // create a mbean server with the given default domain name
        return MBeanServerFactory.createMBeanServer(mBeanServerDefaultDomain);
    }

    protected void createJmxConnector(String host) throws IOException {
        try {
            LocateRegistry.createRegistry(registryPort);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Created JMXConnector RMI regisry on port " + registryPort);
            }
        } catch (RemoteException ex) {
            // The registry may had been created, we could get the registry instead
        }

        // Create an RMI connector and start it
        JMXServiceURL url;

        if (connectorPort > 0) {
            url = new JMXServiceURL("service:jmx:rmi://" + host + ":" + connectorPort + "/jndi/rmi://" + host
                                    + ":" + registryPort + serviceUrlPath);
        } else {
            url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + host + ":" + registryPort
                                    + serviceUrlPath);
        }
        cs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);

        // Start the connector server asynchronously (in a separate thread).
        Thread connectorThread = new Thread() {
            public void run() {
                try {
                    cs.start();
                } catch (IOException ioe) {
                    LOG.warn("Could not start JMXConnector thread.", ioe);
                }
            }
        };
        connectorThread.setName("Camel JMX Connector Thread [" + url + "]");
        connectorThread.start();
        LOG.info("JMX Connector thread started and listening at: " + url);
    }

    public String getMBeanObjectDomainName() {
        return mBeanObjectDomainName;
    }

    public void setServer(MBeanServer value) {
        server = value;
    }

}
