/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.communications.command.server;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.rhq.enterprise.communications.command.CommandType;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;

/**
 * An MBean that maintains a directory of all currently deployed {@link CommandService} MBean services for all
 * subsystems. In effect, it provides a service that allows a client to determine what MBean service provides support
 * and can execute a particular type of command.
 *
 * <p>Each set of command services are organized into separate subsystems. A command service belongs to a subsystem if,
 * in its <code>ObjectName</code>, it has the {@link KeyProperty#SUBSYSTEM subsystem} key property specified. If it does
 * not specify that key property in its name, it is considered in the unnamed, anonymous subsystem.</p>
 *
 * <p>This directory service is used by the {@link CommandProcessor} invocation handler to help determine where to
 * direct a command request for processing.</p>
 *
 * <p>This directory is part of the mechanism by which command services can be dynamically added/updated/removed. This
 * service will listen for command services getting deployed and undeployed to/from the MBeanServer and will dynamically
 * update itself accordingly.</p>
 *
 * @author John Mazzitelli
 */
public class CommandServiceDirectory extends CommandMBean implements CommandServiceDirectoryMBean, NotificationListener {
    /**
     * This is a special subsystem identifier that is used as a key into our directory map. It is used to denote the
     * unnamed, anonymous subsystem name. Note that it has special characters to make sure that it can never be part of
     * a JMX <code>ObjectName</code> (this is to avoid a deployer unknowingly naming his subsystem the same as our
     * special null name). The unnamed, anonymous subsystem is really identified by a <code>null</code> subsystem name.
     * Package scope so {@link CommandServiceDirectoryEntry} can use this.
     */
    static final String NULL_SUBSYSTEM = ",null,";

    /**
     * Contains a map of all supported command types and their associated command services - this is "the directory".
     * This map is keyed on subsystem string - the values of the map are maps themselves. Each subsystem has its own map
     * of command types/command services. Each inner subsystem map is keyed on {@link CommandType command type} with
     * each value being the <code>ObjectName</code> of the {@link CommandServiceMBean} that provides support for the
     * command type. Note that this object is used as a monitor lock when needing to synchronize access to the
     * directory.
     */
    private Map<String, Map<CommandType, ObjectName>> m_allCommandTypes;

    /**
     * If <code>true</code>, any new command service MBean that gets registered will be added to the directory; any
     * current command service MBean that gets deregistered will be removed from the directory. If <code>false</code>,
     * only an initial inventory of the current set of registered command service MBeans will be added to the directory.
     */
    private boolean m_allowDynamicDiscovery;

    /**
     * Creates a new {@link CommandServiceDirectory} object.
     */
    public CommandServiceDirectory() {
        m_allowDynamicDiscovery = false;
        m_allCommandTypes = new HashMap<String, Map<CommandType, ObjectName>>();
    }

    /**
     * Verifies that the <code>ObjectName</code> of this directory MBean has the appropriate key properties and
     * initializes the directory with an inventory of the current set of supported command types/command services.
     *
     * @see MBeanRegistration#preRegister(MBeanServer, ObjectName)
     */
    @Override
    public ObjectName preRegister(MBeanServer mbs, ObjectName name) throws Exception {
        if (!KeyProperty.TYPE_DIRECTORY.equals(name.getKeyProperty(KeyProperty.TYPE))) {
            String errorMsg = getLog().getMsgString(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_INVALID_SELF_NAME,
                name, KeyProperty.TYPE, KeyProperty.TYPE_DIRECTORY);
            throw new IllegalArgumentException(errorMsg);
        }

        return super.preRegister(mbs, name);
    }

    /**
     * Clears the directory of all entries then takes an inventory of existing command services and adds them to the
     * directory.
     *
     * @throws RuntimeException
     */
    @Override
    public void startService() {
        try {
            // lock out all others to prevent new command registrations from getting added until we finish the inventory
            synchronized (m_allCommandTypes) {
                m_allCommandTypes.clear();

                // remember the original value - in case it was previously configured, we want to restore it
                boolean discoveryFlagBackup = m_allowDynamicDiscovery;

                // we are ready to start accepting (un)register notifications to dynamically detect commands being (un)deployed
                // the notification handler will deal with the check for permission for dynamic discovery
                // note that we locked m_allCommandTypes so any incoming notifications will block until we finish inventory
                startListening();

                // take an initial inventory of the command services already deployed
                m_allowDynamicDiscovery = true;
                inventory();

                // by default, we do not allow dynamic discovery for security purposes
                m_allowDynamicDiscovery = discoveryFlagBackup;
            }
        } catch (Exception e) {
            throw new RuntimeException(getLog().getMsgString(
                CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_FAILED_TO_START), e);
        }

        return;
    }

    /**
     * Clean up any resources that were initialized during start.
     */
    @Override
    public void stopService() {
        synchronized (m_allCommandTypes) {
            stopListening();
            m_allCommandTypes.clear();
        }

        return;
    }

    /**
     * As command services are deployed and undeployed, this notification handler will detect this and update the
     * directory accordingly.
     *
     * <p>This method ensures thread-safety during its modifications to the directory data structures.</p>
     *
     * <p>If the directory is not {@link #getAllowDynamicDiscovery() allowed to perform dynamic discovery}, this method
     * does nothing; the notification is ignored.</p>
     *
     * @see NotificationListener#handleNotification(Notification, Object)
     */
    public void handleNotification(Notification notification, Object handback) {
        MBeanServerNotification mbsNotif = null;

        if (notification instanceof MBeanServerNotification) {
            mbsNotif = (MBeanServerNotification) notification;

            // synchronize now so we only have to do it once, any mods done to the directory
            // are thread-safe from this point on
            synchronized (m_allCommandTypes) {
                if (MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(mbsNotif.getType())) {
                    addAllSupportedCommandTypes(mbsNotif.getMBeanName());
                } else if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION.equals(mbsNotif.getType())) {
                    removeAllSupportedCommandTypes(mbsNotif.getMBeanName());
                } else {
                    mbsNotif = null;
                }
            }
        }

        if (mbsNotif == null) {
            getLog().warn(
                getLog().getMsgString(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_UNKNOWN_NOTIF, notification));
        }

        return;
    }

    /**
     * Finds the provider of the given command type by looking it up in the directory in a thread-safe way.
     *
     * @see CommandServiceDirectoryMBean#getCommandTypeProvider(String, CommandType)
     */
    public CommandServiceDirectoryEntry getCommandTypeProvider(String subsystem, CommandType commandType) {
        CommandServiceDirectoryEntry retEntry = null;
        ObjectName commandServiceName = null;

        synchronized (m_allCommandTypes) {
            Map subsystemServices = getSubsystemCommandTypes(subsystem);
            commandServiceName = (ObjectName) subsystemServices.get(commandType);
        }

        if (commandServiceName != null) {
            retEntry = new CommandServiceDirectoryEntry(subsystem, commandType, commandServiceName);
        }

        return retEntry;
    }

    /**
     * @see CommandServiceDirectoryMBean#getSubsystemEntries(String)
     */
    public CommandServiceDirectoryEntry[] getSubsystemEntries(String subsystem) {
        List<CommandServiceDirectoryEntry> entries = new ArrayList<CommandServiceDirectoryEntry>();

        synchronized (m_allCommandTypes) {
            Map subsystemServices = getSubsystemCommandTypes(subsystem);
            for (Iterator iter = subsystemServices.entrySet().iterator(); iter.hasNext();) {
                Map.Entry mapEntry = (Map.Entry) iter.next();
                entries.add(new CommandServiceDirectoryEntry(subsystem, (CommandType) mapEntry.getKey(),
                    (ObjectName) mapEntry.getValue()));
            }
        }

        return entries.toArray(new CommandServiceDirectoryEntry[entries.size()]);
    }

    /**
     * @see CommandServiceDirectoryMBean#getAllEntries()
     */
    public CommandServiceDirectoryEntry[] getAllEntries() {
        List<CommandServiceDirectoryEntry> entries = new ArrayList<CommandServiceDirectoryEntry>();

        synchronized (m_allCommandTypes) {
            for (Iterator iter = m_allCommandTypes.keySet().iterator(); iter.hasNext();) {
                String subsystem = (String) iter.next();
                CommandServiceDirectoryEntry[] subsystemEntries = getSubsystemEntries(subsystem);
                for (int i = 0; i < subsystemEntries.length; i++) {
                    entries.add(subsystemEntries[i]);
                }
            }
        }

        return entries.toArray(new CommandServiceDirectoryEntry[entries.size()]);
    }

    /**
     * @see CommandServiceDirectoryMBean#setAllowDynamicDiscovery(boolean)
     */
    public void setAllowDynamicDiscovery(boolean flag) {
        if (flag) {
            getLog().debug(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_DYNAMIC_DISCOVERY_ALLOWED);
        }

        m_allowDynamicDiscovery = flag;
    }

    /**
     * @see CommandServiceDirectoryMBean#getAllowDynamicDiscovery()
     */
    public boolean getAllowDynamicDiscovery() {
        return m_allowDynamicDiscovery;
    }

    /**
     * Starts listening to the MBeanServer for (un)register notifications. This method registers this object as a
     * notification listener on the MBeanServer delegate.
     *
     * @throws Exception if failed for some reason to register as a listener on the MBeanServer delegate
     */
    private void startListening() throws Exception {
        ObjectName delegate = new ObjectName("JMImplementation:type=MBeanServerDelegate");

        getMBeanServer().addNotificationListener(delegate, this, null, null);

        return;
    }

    /**
     * Stops this MBean from listening for (un)register notifications. This method should be called during the
     * deregistration of this object.
     */
    private void stopListening() {
        try {
            ObjectName delegate = new ObjectName("JMImplementation:type=MBeanServerDelegate");

            getMBeanServer().removeNotificationListener(delegate, this);
        } catch (Exception e) {
            // ignore this to allow for us to continue deregistering; this exception should never occur anyway
            getLog().warn(e, CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_LISTENER_REMOVAL_FAILURE);
        }

        return;
    }

    /**
     * This takes inventory of the command services already deployed in the <code>MBeanServer</code>. All command
     * services deployed in all subsystems will be stored in this directory, mapped to their supported command types.
     *
     * @throws Exception if failed to obtain the full inventory of command services
     */
    private void inventory() throws Exception {
        // an MBean matches our query if its name follows the pattern: *:*,type=command
        // the returned set will contain all the object names of all command service MBeans in any subsystem
        String type = KeyProperty.TYPE + "=" + KeyProperty.TYPE_COMMAND;
        ObjectName query = new ObjectName("*:*," + type);
        Set commandServices = getMBeanServer().queryNames(query, null);

        // for each command service, send a registration notification to simluate the new service getting deploy
        // this will add it to the directory immediately
        for (Iterator iter = commandServices.iterator(); iter.hasNext();) {
            ObjectName newCommandServiceName = (ObjectName) iter.next();
            MBeanServerNotification notif = new MBeanServerNotification(
                MBeanServerNotification.REGISTRATION_NOTIFICATION, this, 0L, newCommandServiceName);
            handleNotification(notif, null);
        }

        return;
    }

    /**
     * Given a command service <code>ObjectName</code>, this will return the subsystem in which the command service
     * belongs.
     *
     * @param  commandServiceName the name of the command service
     *
     * @return the name of the subsystem
     */
    private String getSubsystem(ObjectName commandServiceName) {
        String retSubsystem = commandServiceName.getKeyProperty(KeyProperty.SUBSYSTEM);

        if (retSubsystem == null) {
            retSubsystem = NULL_SUBSYSTEM;
        }

        return retSubsystem;
    }

    /**
     * Returns <code>true</code> if the given <code>ObjectName</code> matches that of a command service. See
     * {@link KeyProperty#TYPE_COMMAND}.
     *
     * @param  name the name of the MBean to test
     *
     * @return <code>true</code> if it appears that the <code>name</code> belongs to a command service, <code>
     *         false</code> otherwise
     */
    private boolean isCommandService(ObjectName name) {
        return (KeyProperty.TYPE_COMMAND.equals(name.getKeyProperty(KeyProperty.TYPE)));
    }

    /**
     * This method invokes a JMX call to the named command service to retrieve its array of all supported
     * {@link CommandType command types} - that array is then returned. Because this actually invokes a JMX call on the
     * given MBean <code>name</code>, that MBean must actually be registered and available (i.e. this method cannot be
     * called if we know the MBean is being deregistered or has been deregistered).
     *
     * <p>Note that if <code>name</code> does not identify a {@link CommandServiceMBean command service}, this method
     * simply returns <code>null</code> (i.e. no exception is thrown).</p>
     *
     * @param  name the <code>ObjectName</code> of the command service
     *
     * @return array of all {@link CommandType}s supported by the command service, or <code>null</code> if the given
     *         <code>name</code> does not correspond to a command service.
     */
    private CommandType[] getCommandServiceCommands(ObjectName name) {
        CommandType[] retComandTypes = null;

        try {
            CommandServiceMBean proxy = (CommandServiceMBean) MBeanServerInvocationHandler.newProxyInstance(
                getMBeanServer(), name, CommandServiceMBean.class, false);
            retComandTypes = proxy.getSupportedCommandTypes();
        } catch (Exception e) {
            // ignore, "name" does not correspond to a command service
            // log at trace mainly for debugging purposes in case we want to see the exception
            getLog().trace(CommI18NResourceKeys.EXCEPTION, e);
        }

        return retComandTypes;
    }

    /**
     * Given a subsystem, this will return a map of all supported command types for that subsystem and the command
     * services that provide them.
     *
     * <p>The returned map is keyed on {@link CommandType command type} and the values are the command service <code>
     * ObjectName</code> s.</p>
     *
     * <p>To find out the command service that provides a given command type, look up the
     * {@link CommandType command type} as the key and get the name of the command service that can execute commands of
     * that type.</p>
     *
     * <p><code>subsystem</code> may be <code>null</code> to denote the unnamed, anonymous subsystem.</p>
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param  subsystem the subsystem map to obtain (may be <code>null</code>)
     *
     * @return map of all supported command types and their command services in the given subsystem
     */
    private Map<CommandType, ObjectName> getSubsystemCommandTypes(String subsystem) {
        if (subsystem == null) {
            subsystem = NULL_SUBSYSTEM;
        }

        Map<CommandType, ObjectName> retSubsystemMap;

        retSubsystemMap = m_allCommandTypes.get(subsystem);

        if (retSubsystemMap == null) {
            retSubsystemMap = new HashMap<CommandType, ObjectName>();
            m_allCommandTypes.put(subsystem, retSubsystemMap);
        }

        return retSubsystemMap;
    }

    /**
     * If the given subsystem no longer has any support command types, it is purged from the directory. If the given
     * subsystem still has one or more supported command types, this method does nothing.
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param subsystem the subsystem to check and purge if empty of supported command types
     */
    private void removeSubsystemCommandTypesIfEmpty(String subsystem) {
        if (subsystem == null) {
            subsystem = NULL_SUBSYSTEM;
        }

        Map subsystemMap = m_allCommandTypes.get(subsystem);
        if (subsystemMap.size() == 0) {
            m_allCommandTypes.remove(subsystem);
        }

        return;
    }

    /**
     * Adds a command type/command service pair to the directory.
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param commandType        the type of command that is being added to the directory
     * @param commandServiceName the name of the service that provides the command type functionality
     */
    private void addSupportedCommandType(CommandType commandType, ObjectName commandServiceName) {
        String subsystem = getSubsystem(commandServiceName);
        Map<CommandType, ObjectName> subsystemMap = getSubsystemCommandTypes(subsystem);

        subsystemMap.put(commandType, commandServiceName);

        getLog().debug(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_NEW_SUPPORTED_COMMAND, commandType,
            commandServiceName);

        return;
    }

    /**
     * Removes a command type/command service pair from the directory.
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param commandType        the type of command that is being removed from the directory
     * @param commandServiceName the name of the service that used to provide the command type functionality
     */
    private void removeSupportedCommandType(CommandType commandType, ObjectName commandServiceName) {
        String subsystem = getSubsystem(commandServiceName);
        Map<CommandType, ObjectName> subsystemMap = getSubsystemCommandTypes(subsystem);
        ObjectName mappedCommandService = subsystemMap.get(commandType);

        if (commandServiceName.equals(mappedCommandService)) {
            // remove the command type, and if no more command types in this subsystem are supported, remove the map itself
            subsystemMap.remove(commandType);

            getLog().debug(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_REMOVED_COMMAND_SUPPORT, commandType,
                commandServiceName);

            removeSubsystemCommandTypesIfEmpty(subsystem);
        } else {
            getLog().warn(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_REMOVAL_FAILURE, commandType,
                commandServiceName, mappedCommandService);
        }

        return;
    }

    /**
     * Determines what (if any) command types the given MBean supports and adds them to the directory. The given MBean
     * may or may not be a {@link CommandServiceMBean}; if it is not, this method simply ignores it and does nothing.
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param name the MBean whose supported command types will be added to the directory
     *
     * @see   #isCommandService(ObjectName)
     */
    private void addAllSupportedCommandTypes(ObjectName name) {
        if (isCommandService(name)) {
            if (m_allowDynamicDiscovery) {
                CommandType[] supportedCommandTypes = getCommandServiceCommands(name);

                if (supportedCommandTypes != null) {
                    for (int i = 0; i < supportedCommandTypes.length; i++) {
                        addSupportedCommandType(supportedCommandTypes[i], name);
                    }
                }
            } else {
                getLog().warn(CommI18NResourceKeys.COMMAND_SERVICE_DIRECTORY_DETECTED_BUT_NOT_ADDED, name);
            }
        }

        return;
    }

    /**
     * Determines what (if any) command types the given MBean supports and removes them from the directory. The given
     * MBean may or may not be a {@link CommandServiceMBean}; if it is not, this method simply ignores it and does
     * nothing.
     *
     * <p>This method is not thread-safe.</p>
     *
     * @param name the MBean whose supported command types will be removed from the directory
     *
     * @see   #isCommandService(ObjectName)
     */
    private void removeAllSupportedCommandTypes(ObjectName name) {
        if (isCommandService(name)) {
            String subsystem = getSubsystem(name);
            Map<CommandType, ObjectName> subsystemMap = getSubsystemCommandTypes(subsystem);
            List<CommandType> doomedCommandTypes = new ArrayList<CommandType>();

            // remember which command types that this command service provided
            Set<Map.Entry<CommandType, ObjectName>> subsystemMapEntrySet = subsystemMap.entrySet();
            for (Map.Entry<CommandType, ObjectName> subsystemMapEntry : subsystemMapEntrySet) {
                if (name.equals(subsystemMapEntry.getValue())) {
                    doomedCommandTypes.add(subsystemMapEntry.getKey());
                }
            }

            // remove each command type from the directory
            for (CommandType doomedCommandType : doomedCommandTypes) {
                removeSupportedCommandType(doomedCommandType, name);
            }
        }

        return;
    }
}