/* * 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; } }