package org.jolokia.backend;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.*;

import javax.management.*;

import org.jolokia.backend.executor.MBeanServerExecutor;
import org.jolokia.backend.executor.NotChangedException;
import org.jolokia.backend.plugin.MBeanPlugin;
import org.jolokia.backend.plugin.MBeanPluginContext;
import org.jolokia.config.ConfigKey;
import org.jolokia.config.Configuration;
import org.jolokia.detector.*;
import org.jolokia.handler.JsonRequestHandler;
import org.jolokia.request.JmxRequest;
import org.jolokia.util.LogHandler;
import org.jolokia.util.ServiceObjectFactory;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

/*
 * Copyright 2009-2013 Roland Huss
 *
 * Licensed 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.
 */


/**
 * Handler for finding and merging various MBeanServers locally when used
 * as an agent.
 *
 * @author roland
 * @since Jun 15, 2009
 */
public class MBeanServerHandler implements MBeanServerHandlerMBean, MBeanRegistration {

    // The object dealing with all MBeanServers
    private MBeanServerExecutorLocal mBeanServerManager;

    // Optional domain for registering this handler as a MBean
    private String qualifier;

    // Information about the server environment
    private ServerHandle serverHandle;

    // Handles remembered for unregistering
    private final List<MBeanHandle> mBeanHandles = new ArrayList<MBeanHandle>();

    /**
     * Create a new MBeanServer handler who is responsible for managing multiple intra VM {@link MBeanServer} at once
     * An optional qualifier used for registering this object as an MBean is taken from the given configuration as well
     *
     * @param pConfig configuration for this agent which is also given to the {@see ServerHandle#postDetect()} method for
     *                special initialization.
     *
     * @param pLogHandler log handler used for logging purposes
     */
    public MBeanServerHandler(Configuration pConfig, LogHandler pLogHandler) {
        // A qualifier, if given, is used to add the MBean Name of this MBean
        qualifier = pConfig.get(ConfigKey.MBEAN_QUALIFIER);
        List<ServerDetector> detectors = lookupDetectors();
        mBeanServerManager = new MBeanServerExecutorLocal(detectors);
        initServerHandle(pConfig, pLogHandler, detectors);
        initMBean();
        initPlugins(pConfig, pLogHandler);
    }

    private void initPlugins(Configuration pConfig, LogHandler pLogHandler) {
        List<MBeanPlugin> plugins = ServiceObjectFactory.createServiceObjects("META-INF/plugins");
        if (plugins.size() > 0) {
            MBeanPluginContext ctx = createMBeanPluginContext();
            Map pluginConfigs = getPluginOptions(pConfig,pLogHandler);
            for (MBeanPlugin plugin : plugins) {
                try {
                    plugin.init(ctx,(Map) pluginConfigs.get(plugin.getId()));
                } catch (JMException exp) {
                    pLogHandler.error("Error while initializing plugin " + plugin.getId(),exp);
                }
            }
        }
    }

    private Map getPluginOptions(Configuration pConfig, LogHandler pLogHandler) {
        String options = pConfig.get(ConfigKey.MBEAN_PLUGIN_OPTIONS);
        try {
            return options != null ? (JSONObject) new JSONParser().parse(options) : new JSONObject();
        } catch (ParseException e) {
            throw new IllegalStateException("Could not parse plugin options '" + options + "' as JSON Objects" + e,e);
        }
    }

    // Delegate to internal objects
    private MBeanPluginContext createMBeanPluginContext() {
        return new MBeanPluginContext() {
            public ObjectName registerMBean(Object pMBean, String... pOptionalName) throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException {
                return MBeanServerHandler.this.registerMBean(pMBean, pOptionalName);
            }

            public void each(ObjectName pObjectName, MBeanEachCallback pCallback) throws IOException, ReflectionException, MBeanException {
                mBeanServerManager.each(pObjectName,pCallback);
            }

            public <R> R call(ObjectName pObjectName, MBeanAction<R> pMBeanAction, Object... pExtraArgs) throws IOException, ReflectionException, MBeanException, AttributeNotFoundException, InstanceNotFoundException {
                return mBeanServerManager.call(pObjectName,pMBeanAction,pExtraArgs);
            }

            public Set<ObjectName> queryNames(ObjectName pObjectName) throws IOException {
                return mBeanServerManager.queryNames(pObjectName);
            }

            public boolean hasMBeansListChangedSince(long pTimestamp) {
                return mBeanServerManager.hasMBeansListChangedSince(pTimestamp);
            }
        };
    }


    /**
     * Initialize the server handle.
     * @param pConfig configuration passed through to the server detectors
     * @param pLogHandler used for putting out diagnostic messags
     * @param pDetectors all detectors known
     */
    private void initServerHandle(Configuration pConfig, LogHandler pLogHandler, List<ServerDetector> pDetectors) {
        serverHandle = detectServers(pDetectors, pLogHandler);
        if (serverHandle != null) {
            serverHandle.postDetect(mBeanServerManager, pConfig, pLogHandler);
        }
    }

    /**
     * Dispatch a request to the MBeanServer which can handle it
     *
     * @param pRequestHandler request handler to be called with an MBeanServer
     * @param pJmxReq the request to dispatch
     * @return the result of the request
     */
    public Object dispatchRequest(JsonRequestHandler pRequestHandler, JmxRequest pJmxReq)
            throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, NotChangedException {
        serverHandle.preDispatch(mBeanServerManager,pJmxReq);
        if (pRequestHandler.handleAllServersAtOnce(pJmxReq)) {
            try {
                return pRequestHandler.handleRequest(mBeanServerManager,pJmxReq);
            } catch (IOException e) {
                throw new IllegalStateException("Internal: IOException " + e + ". Shouldn't happen.",e);
            }
        } else {
            return mBeanServerManager.handleRequest(pRequestHandler, pJmxReq);
        }
    }

    /**
     * Register a MBean under a certain name to the platform MBeanServer
     *
     * @param pMBean MBean to register
     * @param pOptionalName optional name under which the bean should be registered. If not provided,
     * it depends on whether the MBean to register implements {@link javax.management.MBeanRegistration} or
     * not.
     *
     * @return the name under which the MBean is registered.
     */
    public final ObjectName registerMBean(Object pMBean,String ... pOptionalName)
            throws MalformedObjectNameException, NotCompliantMBeanException, InstanceAlreadyExistsException {
        synchronized (mBeanHandles) {
            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
            try {
                String name = pOptionalName != null && pOptionalName.length > 0 ? pOptionalName[0] : null;
                ObjectName registeredName = serverHandle.registerMBeanAtServer(server, pMBean, name);
                mBeanHandles.add(new MBeanHandle(server,registeredName));
                return registeredName;
            } catch (RuntimeException exp) {
                throw new IllegalStateException("Could not register " + pMBean + ": " + exp, exp);
            } catch (MBeanRegistrationException exp) {
                throw new IllegalStateException("Could not register " + pMBean + ": " + exp, exp);
            }
        }
    }

    /**
     * Unregister all previously registered MBean. This is tried for all previously
     * registered MBeans
     *
     * @throws JMException if an exception occurs during unregistration
     */
    public final void destroy() throws JMException {
        synchronized (mBeanHandles) {
            List<JMException> exceptions = new ArrayList<JMException>();
            List<MBeanHandle> unregistered = new ArrayList<MBeanHandle>();
            for (MBeanHandle handle : mBeanHandles) {
                try {
                    unregistered.add(handle);
                    handle.server.unregisterMBean(handle.objectName);
                } catch (InstanceNotFoundException e) {
                    exceptions.add(e);
                } catch (MBeanRegistrationException e) {
                    exceptions.add(e);
                }
            }
            // Remove all successfully unregistered handles
            mBeanHandles.removeAll(unregistered);

            // Throw error if any exception occured during unregistration
            if (exceptions.size() == 1) {
                throw exceptions.get(0);
            } else if (exceptions.size() > 1) {
                StringBuilder ret = new StringBuilder();
                for (JMException e : exceptions) {
                    ret.append(e.getMessage()).append(", ");
                }
                throw new JMException(ret.substring(0, ret.length() - 2));
            }
        }

        // Unregister any notification listener
        mBeanServerManager.destroy();
    }

    /**
     * Get the set of MBeanServers found
     *
     * @return set of mbean servers
     */
    public MBeanServerExecutorLocal getMBeanServerManager() {
        return mBeanServerManager;
    }

    /**
     * Get information about the detected server this agent is running on.
     *
     * @return the server info if detected or <code>null</code> if no server
     *          could be detected.
     */
    public ServerHandle getServerHandle() {
        return serverHandle;
    }

    // =================================================================================

    /**
     * Initialise this server handler and register as an MBean
     */
    private void initMBean()  {
        try {
            registerMBean(this,getObjectName());
        } catch (InstanceAlreadyExistsException exp) {
            // This is no problem, since this MBeanServerHandlerMBean holds only global information
            // with no special state (so all instances of this MBean behave similar)
            // This exception can happen, when multiple servlets get registered within the same JVM
        } catch (MalformedObjectNameException e) {
            // Cannot happen, otherwise this is a bug. We should be always provide our own name in correct
            // form.
            throw new IllegalStateException("Internal Error: Own ObjectName " + getObjectName() + " is malformed",e);
        } catch (NotCompliantMBeanException e) {
            // Same here
            throw new IllegalStateException("Internal Error: " + this.getClass().getName() + " is not a compliant MBean",e);
        }
    }

    // Lookup all registered detectors + a default detector
    public static List<ServerDetector> lookupDetectors() {
        List<ServerDetector> detectors =
                ServiceObjectFactory.createServiceObjects("META-INF/detectors-default", "META-INF/detectors");
        // An detector at the end of the chain in order to get a default handle
        detectors.add(new FallbackServerDetector());
        return detectors;
    }

    private List<MBeanPlugin> lookupMBeanPlugins() {
        return ServiceObjectFactory.createServiceObjects("META-INF/mbean-plugins");
    }

    // Detect the server by delegating it to a set of predefined detectors. These will be created
    // by a lookup mechanism, queried and thrown away after this method
    private ServerHandle detectServers(List<ServerDetector> pDetectors, LogHandler pLogHandler) {
        // Now detect the server
        for (ServerDetector detector : pDetectors) {
            try {
                ServerHandle info = detector.detect(mBeanServerManager);
                if (info != null) {
                    return info;
                }
            } catch (Exception exp) {
                // We are defensive here and wont stop the servlet because
                // there is a problem with the server detection. A error will be logged
                // nevertheless, though.
                pLogHandler.error("Error while using detector " + detector.getClass().getSimpleName() + ": " + exp,exp);
            }
        }
        return null;
    }

    // =====================================================================================

    // MBean exported debugging method

    /**
     * Get a description of all MBeanServers found
     * @return a description of all MBeanServers along with their stored MBeans
     */
    public String mBeanServersInfo() {
        return mBeanServerManager.getServersInfo();
    }

    // ==============================================================================================
    // Needed for providing the name for our MBean
    /** {@inheritDoc} */
    public ObjectName preRegister(MBeanServer server, ObjectName name) throws MalformedObjectNameException {
        return new ObjectName(getObjectName());
    }

    /** {@inheritDoc} */
    public final String getObjectName() {
        return OBJECT_NAME + (qualifier != null ? "," + qualifier : "");
    }

    /** {@inheritDoc} */
    public void postRegister(Boolean registrationDone) {
    }

    /** {@inheritDoc} */
    public void preDeregister() {
    }

    /** {@inheritDoc} */
    public void postDeregister() {
    }

    // ==================================================================================
    // Handle for remembering registered MBeans
    private static final class MBeanHandle {
        private ObjectName objectName;
        private MBeanServer server;

        private MBeanHandle(MBeanServer pServer, ObjectName pRegisteredName) {
            server = pServer;
            objectName = pRegisteredName;
        }
    }

    // ==================================================================================
    // Fallback server detector which matches always

    private static class NullServerHandle extends ServerHandle {
        /**
         * Empty constructor initializing the server handle completely with null values.
         */
        public NullServerHandle() {
            super(null,null,null, null);
        }
    }

    private static class FallbackServerDetector extends AbstractServerDetector {
        /** {@inheritDoc}
         * @param pMBeanServerExecutor*/
        public ServerHandle detect(MBeanServerExecutor pMBeanServerExecutor) {
            return new NullServerHandle();
        }
    }
}
