blob: 268093ded1fb25138b130e43f5b4a426404a5f30 [file] [log] [blame]
/*
* 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.SystemFailure;
import org.apache.geode.admin.RuntimeAdminException;
import org.apache.geode.annotations.internal.MakeNotStatic;
import org.apache.geode.internal.ClassPathLoader;
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 final String REFRESH_TIMER_NAME = DEFAULT_DOMAIN + ":type=RefreshTimer";
/* indicates whether the mbeanServer, registry & refreshTimer are started */
@MakeNotStatic
private static boolean isStarted;
/** The Commons-Modeler configuration registry for our managed beans */
@MakeNotStatic
private static Registry registry;
/** The <code>MBeanServer</code> for this application */
@MakeNotStatic
private static MBeanServer mbeanServer;
/** MBean name of the Timer which handles refresh notifications */
@MakeNotStatic
private static ObjectName refreshTimerObjectName;
/** Actual TimerMBean responsible for refresh notifications */
@MakeNotStatic
private static TimerMBean refreshTimer;
/**
* Map of ObjectNames to current timerNotificationIds
* <p>
* map: key=ObjectName, value=map: key=RefreshNotificationType, value=timerNotificationId
*/
@MakeNotStatic
private static final Map<NotificationListener, Map<RefreshNotificationType, Integer>> refreshClients =
new HashMap<NotificationListener, Map<RefreshNotificationType, Integer>>();
/** key=ObjectName, value=ManagedResource */
@MakeNotStatic
private static final 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(
"MBean Server is not initialized yet.");
}
registry.setMBeanServer(mbeanServer);
String mbeansResource = getOSPath("/org/apache/geode/admin/jmx/mbeans-descriptors.xml");
URL url = ClassPathLoader.getLatest().getResource(MBeanUtil.class, mbeansResource);
raiseOnFailure(url != null, String.format("Failed to find %s",
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,
String.format("Failed to load metadata from %s",
new Object[] {mbeansResource}));
} catch (Exception e) {
logStackTrace(Level.WARN, e);
throw new RuntimeAdminException(
"Failed to get MBean Registry", 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(String.format("%s in '%s'",
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(
String.format("Failed to create MBean representation for resource %s.",
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),
String.format("Could not find a MBean registered with ObjectName: %s.",
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(
"ManagedBean is null");
}
if (managed == null) {
throw new IllegalArgumentException(
"ManagedBean is null");
}
// 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, long refreshInterval) {
if (client == null) {
throw new IllegalArgumentException(
"NotificationListener is required");
}
if (type == null) {
throw new IllegalArgumentException(
"RefreshNotificationType is required");
}
if (refreshTimerObjectName == null || refreshTimer == null) {
throw new IllegalStateException(
"RefreshTimer has not been properly initialized.");
}
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,
"Could not find registered RefreshTimer instance.");
}
}
// 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 * 1000L), // first occurrence
refreshInterval * 1000L); // 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) {
createRefreshTimer();
}
raiseOnFailure(refreshTimer != null, "Failed to validate Refresh Timer");
if (mbeanServer != null && !mbeanServer.isRegistered(refreshTimerObjectName)) {
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,
"Failed to create/register/start refresh timer.");
} catch (JMRuntimeException e) {
logStackTrace(Level.WARN, e,
"Failed to create/register/start refresh timer.");
} catch (Exception e) {
logStackTrace(Level.WARN, e,
"Failed to create/register/start refresh timer.");
}
}
/**
* 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,
String.format("Failed while unregistering MBean with ObjectName : %s",
new Object[] {objectName}));
} catch (InstanceNotFoundException e) {
logStackTrace(Level.WARN, null,
String.format("While unregistering, could not find MBean with ObjectName : %s",
new Object[] {objectName}));
} catch (JMRuntimeException e) {
logStackTrace(Level.WARN, null,
String.format("Could not un-register MBean with ObjectName : %s",
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,
String.format("While unregistering, could not find MBean with ObjectName : %s",
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() {
@Override
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(String.format("%s is not a ManagedResource",
new Object[] {entry.getClass().getName()}));
}
ManagedResource resource = (ManagedResource) entry;
{
// call cleanup on managedResource
cleanupResource(resource);
}
}
}
}
}, null, null);
} catch (JMException e) {
logStackTrace(Level.WARN, e,
"Failed to register ServerNotificationListener.");
} catch (JMRuntimeException e) {
logStackTrace(Level.WARN, e,
"Failed to register ServerNotificationListener.");
}
}
/**
* 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);
}
}
}