/* * 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.geode.admin.jmx.internal; import java.net.URL; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.management.InstanceNotFoundException; import javax.management.JMException; import javax.management.JMRuntimeException; import javax.management.ListenerNotFoundException; import javax.management.MBeanRegistrationException; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; import javax.management.MBeanServerNotification; import javax.management.MalformedObjectNameException; import javax.management.Notification; import javax.management.NotificationListener; import javax.management.ObjectName; import javax.management.timer.TimerMBean; import org.apache.commons.modeler.ManagedBean; import org.apache.commons.modeler.Registry; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.geode.LogWriter; import org.apache.geode.SystemFailure; import org.apache.geode.admin.RuntimeAdminException; import org.apache.geode.admin.internal.AdminDistributedSystemImpl; import org.apache.geode.internal.ClassPathLoader; import org.apache.geode.internal.i18n.LocalizedStrings; import org.apache.geode.internal.logging.InternalLogWriter; import org.apache.geode.internal.logging.LogService; /** * Common support for MBeans and {@link ManagedResource}s. Static loading of this class creates the * MBeanServer and Modeler Registry. * * @since GemFire 3.5 * */ public class MBeanUtil { private static final Logger logger = LogService.getLogger(); /** The default MBeanServer domain name is "GemFire" */ private static final String DEFAULT_DOMAIN = "GemFire"; /** MBean Name for refreshTimer */ private static String REFRESH_TIMER_NAME = DEFAULT_DOMAIN + ":type=RefreshTimer"; /* indicates whether the mbeanServer, registry & refreshTimer are started */ private static boolean isStarted; /** The Commons-Modeler configuration registry for our managed beans */ private static Registry registry; /** The <code>MBeanServer</code> for this application */ private static MBeanServer mbeanServer; /** MBean name of the Timer which handles refresh notifications */ private static ObjectName refreshTimerObjectName; /** Actual TimerMBean responsible for refresh notifications */ private static TimerMBean refreshTimer; /** * Map of ObjectNames to current timerNotificationIds * <p> * map: key=ObjectName, value=map: key=RefreshNotificationType, value=timerNotificationId */ private static Map<NotificationListener, Map<RefreshNotificationType, Integer>> refreshClients = new HashMap<NotificationListener, Map<RefreshNotificationType, Integer>>(); /** key=ObjectName, value=ManagedResource */ private final static Map<ObjectName, ManagedResource> managedResources = new HashMap<ObjectName, ManagedResource>(); static { try { refreshTimerObjectName = ObjectName.getInstance(REFRESH_TIMER_NAME); } catch (Exception e) { logStackTrace(Level.ERROR, e); } } /** * Initializes Mbean Server, Registry, Refresh Timer & registers Server Notification Listener. * * @return reference to the mbeanServer */ static MBeanServer start() { if (!isStarted) { mbeanServer = createMBeanServer(); registry = createRegistry(); registerServerNotificationListener(); createRefreshTimer(); isStarted = true; } return mbeanServer; } /** * Stops Registry, Refresh Timer. Releases Mbean Server after. */ static void stop() { if (isStarted) { stopRefreshTimer(); registry.stop(); registry = null; releaseMBeanServer();// makes mbeanServer null isStarted = false; } } /** * Create and configure (if necessary) and return the <code>MBeanServer</code> with which we will * be registering our <code>ModelMBean</code> implementations. * * @see javax.management.MBeanServer */ static synchronized MBeanServer createMBeanServer() { if (mbeanServer == null) { mbeanServer = MBeanServerFactory.createMBeanServer(DEFAULT_DOMAIN); } return mbeanServer; } /** * Create and configure (if necessary) and return the Commons-Modeler registry of managed object * descriptions. * * @see org.apache.commons.modeler.Registry */ static synchronized Registry createRegistry() { if (registry == null) { try { registry = Registry.getRegistry(null, null); if (mbeanServer == null) { throw new IllegalStateException( LocalizedStrings.MBeanUtil_MBEAN_SERVER_NOT_INITIALIZED_YET.toLocalizedString()); } registry.setMBeanServer(mbeanServer); String mbeansResource = getOSPath("/org/apache/geode/admin/jmx/mbeans-descriptors.xml"); // System.out.println(LocalizedStrings.MBeanUtil_LOADING_RESOURCE_0.toLocalizedString(mbeansResource)); URL url = ClassPathLoader.getLatest().getResource(MBeanUtil.class, mbeansResource); raiseOnFailure(url != null, LocalizedStrings.MBeanUtil_FAILED_TO_FIND_0 .toLocalizedString(new Object[] {mbeansResource})); registry.loadMetadata(url); // simple test to make sure the xml was actually loaded and is valid... String[] test = registry.findManagedBeans(); raiseOnFailure(test != null && test.length > 0, LocalizedStrings.MBeanUtil_FAILED_TO_LOAD_0 .toLocalizedString(new Object[] {mbeansResource})); } catch (Exception e) { logStackTrace(Level.WARN, e); throw new RuntimeAdminException( LocalizedStrings.MBeanUtil_FAILED_TO_GET_MBEAN_REGISTRY.toLocalizedString(), e); } } return registry; } /** * Creates and registers a <code>ModelMBean</code> for the specified <code>ManagedResource</code>. * State changing callbacks into the <code>ManagedResource</code> will also be made. * * @param resource the ManagedResource to create a managing MBean for * * @return The object name of the newly-created MBean * * @see ManagedResource#setModelMBean */ static ObjectName createMBean(ManagedResource resource) { return createMBean(resource, lookupManagedBean(resource)); } /** * Creates and registers a <code>ModelMBean</code> for the specified <code>ManagedResource</code>. * State changing callbacks into the <code>ManagedResource</code> will also be made. * * @param resource the ManagedResource to create a managing MBean for * @param managed the ManagedBean definition to create the MBean with * @see ManagedResource#setModelMBean */ static ObjectName createMBean(ManagedResource resource, ManagedBean managed) { try { DynamicManagedBean mb = new DynamicManagedBean(managed); resource.setModelMBean(mb.createMBean(resource)); // create the ObjectName and register the MBean... final ObjectName objName; try { objName = ObjectName.getInstance(resource.getMBeanName()); } catch (MalformedObjectNameException e) { throw new MalformedObjectNameException(LocalizedStrings.MBeanUtil_0_IN_1 .toLocalizedString(new Object[] {e.getMessage(), resource.getMBeanName()})); } synchronized (MBeanUtil.class) { // Only register a bean once. Otherwise, you risk race // conditions with things like the RMI connector accessing it. if (mbeanServer != null && !mbeanServer.isRegistered(objName)) { mbeanServer.registerMBean(resource.getModelMBean(), objName); synchronized (managedResources) { managedResources.put(objName, resource); } } } return objName; } catch (java.lang.Exception e) { throw new RuntimeAdminException(LocalizedStrings.MBeanUtil_FAILED_TO_CREATE_MBEAN_FOR_0 .toLocalizedString(new Object[] {resource.getMBeanName()}), e); } } /** * Ensures that an MBean is registered for the specified <code>ManagedResource</code>. If an MBean * cannot be found in the <code>MBeanServer</code>, then this creates and registers a * <code>ModelMBean</code>. State changing callbacks into the <code>ManagedResource</code> will * also be made. * * @param resource the ManagedResource to create a managing MBean for * * @return The object name of the MBean that manages the ManagedResource * * @see ManagedResource#setModelMBean */ static ObjectName ensureMBeanIsRegistered(ManagedResource resource) { try { ObjectName objName = ObjectName.getInstance(resource.getMBeanName()); synchronized (MBeanUtil.class) { if (mbeanServer != null && !mbeanServer.isRegistered(objName)) { return createMBean(resource); } } raiseOnFailure(mbeanServer.isRegistered(objName), LocalizedStrings.MBeanUtil_COULDNT_FIND_MBEAN_REGISTERED_WITH_OBJECTNAME_0 .toLocalizedString(new Object[] {objName.toString()})); return objName; } catch (java.lang.Exception e) { throw new RuntimeAdminException(e); } } /** * Retrieves the <code>ManagedBean</code> configuration from the Registry for the specified * <code>ManagedResource</code> * * @param resource the ManagedResource to find the configuration for */ static ManagedBean lookupManagedBean(ManagedResource resource) { // find the registry defn for our MBean... ManagedBean managed = null; if (registry != null) { managed = registry.findManagedBean(resource.getManagedResourceType().getClassTypeName()); } else { throw new IllegalArgumentException( LocalizedStrings.MBeanUtil_MANAGEDBEAN_IS_NULL.toLocalizedString()); } if (managed == null) { throw new IllegalArgumentException( LocalizedStrings.MBeanUtil_MANAGEDBEAN_IS_NULL.toLocalizedString()); } // customize the defn... managed.setClassName("org.apache.geode.admin.jmx.internal.MX4JModelMBean"); return managed; } /** * Registers a refresh notification for the specified client MBean. Specifying zero for the * refreshInterval disables notification for the refresh client. Note: this does not currently * support remote connections. * * @param client client to listen for refresh notifications * @param userData userData to register with the Notification * @param type refresh notification type the client will use * @param refreshInterval the seconds between refreshes */ static void registerRefreshNotification(NotificationListener client, Object userData, RefreshNotificationType type, int refreshInterval) { if (client == null) { throw new IllegalArgumentException( LocalizedStrings.MBeanUtil_NOTIFICATIONLISTENER_IS_REQUIRED.toLocalizedString()); } if (type == null) { throw new IllegalArgumentException( LocalizedStrings.MBeanUtil_REFRESHNOTIFICATIONTYPE_IS_REQUIRED.toLocalizedString()); } if (refreshTimerObjectName == null || refreshTimer == null) { throw new IllegalStateException( LocalizedStrings.MBeanUtil_REFRESHTIMER_HAS_NOT_BEEN_PROPERLY_INITIALIZED .toLocalizedString()); } try { // get the notifications for the specified client... Map<RefreshNotificationType, Integer> notifications = null; synchronized (refreshClients) { notifications = (Map<RefreshNotificationType, Integer>) refreshClients.get(client); } if (notifications == null) { // If refreshInterval is being set to zero and notifications is removed return if (refreshInterval <= 0) { return; } // never registered before, so add client... notifications = new HashMap<RefreshNotificationType, Integer>(); synchronized (refreshClients) { refreshClients.put(client, notifications); } validateRefreshTimer(); try { // register client as a listener with MBeanServer... mbeanServer.addNotificationListener(refreshTimerObjectName, // timer to listen to client, // the NotificationListener object null, // optional NotificationFilter TODO: convert to using new Object() // not used but null throws IllegalArgumentException ); } catch (InstanceNotFoundException e) { // should not happen since we already checked refreshTimerObjectName logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_COULD_NOT_FIND_REGISTERED_REFRESHTIMER_INSTANCE .toLocalizedString()); } } // TODO: change to manipulating timer indirectly thru mserver... // check for pre-existing refresh notification entry... Integer timerNotificationId = (Integer) notifications.get(type); if (timerNotificationId != null) { try { // found one, so let's remove it... refreshTimer.removeNotification(timerNotificationId); } catch (InstanceNotFoundException e) { // that's ok cause we just wanted to remove it anyway } finally { // null out the map entry for that notification type... notifications.put(type, null); } } if (refreshInterval > 0) { // add notification to the refresh timer... timerNotificationId = refreshTimer.addNotification(type.getType(), // type type.getMessage(), // message = "refresh" userData, // userData new Date(System.currentTimeMillis() + refreshInterval * 1000), // first occurence refreshInterval * 1000); // period to repeat // put an entry into the map for the listener... notifications.put(type, timerNotificationId); } else { // do nothing! refreshInterval must be over 0 to do anything... } } catch (java.lang.RuntimeException e) { logStackTrace(Level.WARN, e); throw e; } catch (VirtualMachineError err) { SystemFailure.initiateFailure(err); // If this ever returns, rethrow the error. We're poisoned // now, so don't let this thread continue. throw err; } catch (java.lang.Error e) { // Whenever you catch Error or Throwable, you must also // catch VirtualMachineError (see above). However, there is // _still_ a possibility that you are dealing with a cascading // error condition, so you also need to check to see if the JVM // is still usable: SystemFailure.checkFailure(); logStackTrace(Level.ERROR, e); throw e; } } /** * Verifies a refresh notification for the specified client MBean. If notification is not * registered, then returns a false * * @param client client to listen for refresh notifications * @param type refresh notification type the client will use * * @return isRegistered boolean indicating if a notification is registered */ static boolean isRefreshNotificationRegistered(NotificationListener client, RefreshNotificationType type) { boolean isRegistered = false; // get the notifications for the specified client... Map<RefreshNotificationType, Integer> notifications = null; synchronized (refreshClients) { notifications = (Map<RefreshNotificationType, Integer>) refreshClients.get(client); } // never registered before if null ... if (notifications != null) { // check for pre-existing refresh notification entry... Integer timerNotificationId = notifications.get(type); if (timerNotificationId != null) { isRegistered = true; } } return isRegistered; } /** * Validates refreshTimer has been registered without problems and attempts to re-register if * there is a problem. */ static void validateRefreshTimer() { if (refreshTimerObjectName == null || refreshTimer == null) { // if (refreshTimerObjectName == null) System.out.println("refreshTimerObjectName is null"); // if (refreshTimer == null) System.out.println("refreshTimer is null"); // System.out.println("[validateRefreshTimer] createRefreshTimer"); createRefreshTimer(); } raiseOnFailure(refreshTimer != null, "Failed to validate Refresh Timer"); if (mbeanServer != null && !mbeanServer.isRegistered(refreshTimerObjectName)) { // System.out.println("[validateRefreshTimer] registerMBean"); try { mbeanServer.registerMBean(refreshTimer, refreshTimerObjectName); } catch (JMException e) { logStackTrace(Level.WARN, e); } catch (JMRuntimeException e) { logStackTrace(Level.WARN, e); } } } /** * Initializes the timer for sending refresh notifications. */ static void createRefreshTimer() { try { refreshTimer = new javax.management.timer.Timer(); mbeanServer.registerMBean(refreshTimer, refreshTimerObjectName); refreshTimer.start(); } catch (JMException e) { logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_FAILED_TO_CREATE_REFRESH_TIMER.toLocalizedString()); } catch (JMRuntimeException e) { logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_FAILED_TO_CREATE_REFRESH_TIMER.toLocalizedString()); } catch (Exception e) { logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_FAILED_TO_CREATE_REFRESH_TIMER.toLocalizedString()); } } /** * Initializes the timer for sending refresh notifications. */ static void stopRefreshTimer() { try { if (refreshTimer != null && mbeanServer != null) { mbeanServer.unregisterMBean(refreshTimerObjectName); refreshTimer.stop(); } } catch (JMException e) { logStackTrace(Level.WARN, e); } catch (JMRuntimeException e) { logStackTrace(Level.WARN, e); } catch (Exception e) { logStackTrace(Level.DEBUG, e, "Failed to stop refresh timer for MBeanUtil"); } } /** * Return a String that been modified to be compliant as a property of an ObjectName. * <p> * The property name of an ObjectName may not contain any of the following characters: <b><i>: , = * * ?</i></b> * <p> * This method will replace the above non-compliant characters with a dash: <b><i>-</i></b> * <p> * If value is empty, this method will return the string "nothing". * <p> * Note: this is <code>public</code> because certain tests call this from outside of the package. * TODO: clean this up * * @param value the potentially non-compliant ObjectName property * @return the value modified to be compliant as an ObjectName property */ public static String makeCompliantMBeanNameProperty(String value) { value = value.replace(':', '-'); value = value.replace(',', '-'); value = value.replace('=', '-'); value = value.replace('*', '-'); value = value.replace('?', '-'); if (value.length() < 1) { value = "nothing"; } return value; } /** * Unregisters all GemFire MBeans and then releases the MBeanServer for garbage collection. */ static void releaseMBeanServer() { try { // unregister all GemFire mbeans... Iterator iter = mbeanServer.queryNames(null, null).iterator(); while (iter.hasNext()) { ObjectName name = (ObjectName) iter.next(); if (name.getDomain().startsWith(DEFAULT_DOMAIN)) { unregisterMBean(name); } } // last, release the mbean server... MBeanServerFactory.releaseMBeanServer(mbeanServer); mbeanServer = null; } catch (JMRuntimeException e) { logStackTrace(Level.WARN, e); } /* * See #42391. Cleaning up the static maps which might be still holding references to * ManagedResources */ synchronized (MBeanUtil.managedResources) { MBeanUtil.managedResources.clear(); } synchronized (refreshClients) { refreshClients.clear(); } /* * See #42391. Cleaning up the static maps which might be still holding references to * ManagedResources */ synchronized (MBeanUtil.managedResources) { MBeanUtil.managedResources.clear(); } synchronized (refreshClients) { refreshClients.clear(); } } /** * Returns true if a MBean with given ObjectName is registered. * * @param objectName ObjectName to use for checking if MBean is registered * @return true if MBeanServer is not null & MBean with given ObjectName is registered with the * MBeanServer */ static boolean isRegistered(ObjectName objectName) { return mbeanServer != null && mbeanServer.isRegistered(objectName); } /** * Unregisters the identified MBean if it's registered. */ static void unregisterMBean(ObjectName objectName) { try { if (mbeanServer != null && mbeanServer.isRegistered(objectName)) { mbeanServer.unregisterMBean(objectName); } } catch (MBeanRegistrationException e) { logStackTrace(Level.WARN, null, LocalizedStrings.MBeanUtil_FAILED_WHILE_UNREGISTERING_MBEAN_WITH_OBJECTNAME_0 .toLocalizedString(new Object[] {objectName})); } catch (InstanceNotFoundException e) { logStackTrace(Level.WARN, null, LocalizedStrings.MBeanUtil_WHILE_UNREGISTERING_COULDNT_FIND_MBEAN_WITH_OBJECTNAME_0 .toLocalizedString(new Object[] {objectName})); } catch (JMRuntimeException e) { logStackTrace(Level.WARN, null, LocalizedStrings.MBeanUtil_COULD_NOT_UNREGISTER_MBEAN_WITH_OBJECTNAME_0 .toLocalizedString(new Object[] {objectName})); } } static void unregisterMBean(ManagedResource resource) { if (resource != null) { unregisterMBean(resource.getObjectName()); // call cleanup on managedResource here and not rely on listener // since it is possible that notification listener not deliver // all notifications of un-registration. If resource is // cleaned here, another call from the listener should be as good as a no-op cleanupResource(resource); } } // cleanup resource private static void cleanupResource(ManagedResource resource) { synchronized (MBeanUtil.managedResources) { MBeanUtil.managedResources.remove(resource.getObjectName()); } resource.cleanupResource(); // get the notifications for the specified client... Map<RefreshNotificationType, Integer> notifications = null; synchronized (refreshClients) { notifications = (Map<RefreshNotificationType, Integer>) refreshClients.remove(resource); } // never registered before if null ... // Also as of current, there is ever only 1 Notification type per // MBean, so we do need need a while loop here if (notifications != null) { // Fix for findbugs reported inefficiency with keySet(). Set<Map.Entry<RefreshNotificationType, Integer>> entries = notifications.entrySet(); for (Map.Entry<RefreshNotificationType, Integer> e : entries) { Integer timerNotificationId = e.getValue(); if (null != timerNotificationId) { try { // found one, so let's remove it... refreshTimer.removeNotification(timerNotificationId); } catch (InstanceNotFoundException xptn) { // that's ok cause we just wanted to remove it anyway logStackTrace(Level.DEBUG, xptn); } } } try { if (mbeanServer != null && mbeanServer.isRegistered(refreshTimerObjectName)) { // remove client as a listener with MBeanServer... mbeanServer.removeNotificationListener(refreshTimerObjectName, // timer to listen to (NotificationListener) resource // the NotificationListener object ); } } catch (ListenerNotFoundException xptn) { // should not happen since we already checked refreshTimerObjectName logStackTrace(Level.WARN, null, xptn.getMessage()); } catch (InstanceNotFoundException xptn) { // should not happen since we already checked refreshTimerObjectName logStackTrace(Level.WARN, null, LocalizedStrings.MBeanUtil_WHILE_UNREGISTERING_COULDNT_FIND_MBEAN_WITH_OBJECTNAME_0 .toLocalizedString(new Object[] {refreshTimerObjectName})); } } } // ----- borrowed the following from admin.internal.RemoteCommand ----- /** Translates the path between Windows and UNIX. */ static String getOSPath(String path) { if (pathIsWindows(path)) { return path.replace('/', '\\'); } else { return path.replace('\\', '/'); } } /** Returns true if the path is on Windows. */ static boolean pathIsWindows(String path) { if (path != null && path.length() > 1) { return (Character.isLetter(path.charAt(0)) && path.charAt(1) == ':') || (path.startsWith("//") || path.startsWith("\\\\")); } return false; } static void registerServerNotificationListener() { if (mbeanServer == null) { return; } try { // the MBeanServerDelegate name is spec'ed as the following... ObjectName delegate = ObjectName.getInstance("JMImplementation:type=MBeanServerDelegate"); mbeanServer.addNotificationListener(delegate, new NotificationListener() { public void handleNotification(Notification notification, Object handback) { MBeanServerNotification serverNotification = (MBeanServerNotification) notification; if (MBeanServerNotification.UNREGISTRATION_NOTIFICATION .equals(serverNotification.getType())) { ObjectName objectName = serverNotification.getMBeanName(); synchronized (MBeanUtil.managedResources) { Object entry = MBeanUtil.managedResources.get(objectName); if (entry == null) return; if (!(entry instanceof ManagedResource)) { throw new ClassCastException(LocalizedStrings.MBeanUtil_0_IS_NOT_A_MANAGEDRESOURCE .toLocalizedString(new Object[] {entry.getClass().getName()})); } ManagedResource resource = (ManagedResource) entry; { // call cleanup on managedResource cleanupResource(resource); } } } } }, null, null); } catch (JMException e) { logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_FAILED_TO_REGISTER_SERVERNOTIFICATIONLISTENER .toLocalizedString()); } catch (JMRuntimeException e) { logStackTrace(Level.WARN, e, LocalizedStrings.MBeanUtil_FAILED_TO_REGISTER_SERVERNOTIFICATIONLISTENER .toLocalizedString()); } } /** * Logs the stack trace for the given Throwable if logger is initialized else prints the stack * trace using System.out. * * @param level severity level to log at * @param throwable Throwable to log stack trace for */ public static void logStackTrace(Level level, Throwable throwable) { logStackTrace(level, throwable, null); } /** * Logs the stack trace for the given Throwable if logger is initialized else prints the stack * trace using System.out. * * @param level severity level to log at * @param throwable Throwable to log stack trace for * @param message user friendly error message to show */ public static void logStackTrace(Level level, Throwable throwable, String message) { logger.log(level, message, throwable); } /** * Raises RuntimeAdminException with given 'message' if given 'condition' is false. * * @param condition condition to evaluate * @param message failure message */ private static void raiseOnFailure(boolean condition, String message) { if (!condition) { throw new RuntimeAdminException(message); } } }