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

