/*
* RHQ Management Platform
* Copyright (C) 2005-2011 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, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* 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 and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.plugins.jmx;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.openmbean.CompositeData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.mc4j.ems.connection.bean.operation.EmsOperation;
import org.mc4j.ems.connection.bean.parameter.EmsParameter;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.util.exception.ThrowableUtil;
/**
* A generic JMX MBean resource component that can be used to manage a JMX MBean. The resource's plugin configuration
* will determine what MBean is to be managed by this component.
*
* @author Greg Hinkle
* @author John Mazzitelli
*/
public class MBeanResourceComponent<T extends JMXComponent<?>> implements MeasurementFacet, OperationFacet,
ConfigurationFacet, JMXComponent<T> {
/**
* Subclasses are free to use this directly as a way to log messages.
*/
protected static Log log = LogFactory.getLog(MBeanResourceComponent.class);
public static final String OBJECT_NAME_PROP = "objectName";
public static final String PROPERTY_TRANSFORM = "propertyTransform";
private static final Pattern PROPERTY_PATTERN = Pattern.compile("^\\{(?:\\{([^\\}]*)\\})?([^\\}]*)\\}$");
private static final Pattern TEMPLATE_PATTERN = Pattern.compile("%([^%]+)%");
private static final String CALCULATED_METRIC_HEAP_USAGE_PERCENTAGE = "Calculated.HeapUsagePercentage";
// these two should be private - subclasses need to override the getter/setter/load methods to affect these
/**
* @deprecated do not use this - use {@link #getEmsBean()} instead
*/
@Deprecated
protected EmsBean bean;
/**
* @deprecated do not use this - use {@link #getResourceContext()} instead
*/
@Deprecated
protected ResourceContext<T> resourceContext;
/**
* Stores the context and loads the MBean.
* @see ResourceComponent#start(ResourceContext)
*/
public void start(ResourceContext<T> context) {
setResourceContext(context);
setEmsBean(loadBean());
}
/**
* Cleans the old resource context and the old MBean.
* @see ResourceComponent#stop()
*/
public void stop() {
setResourceContext(null);
setEmsBean(null);
}
/**
* Gets the loaded MBean. This will attempt to {@link #loadBean() load} the bean if it
* is not yet loaded. This might still return <code>null</code> if the MBean could
* not be loaded.
*
* @return the loaded MBean
* @throws IllegalStateException if it could not be loaded
*
* @see #loadBean()
*/
public EmsBean getEmsBean() {
// make sure the connection used to cache the bean is still the current connection. if not, re-cache the bean
EmsConnection beanConn = (null != this.bean) ? this.bean.getConnectionProvider().getExistingConnection() : null;
EmsConnection currConn = (null != this.bean) ? getEmsConnection() : null;
if ((null == this.bean) || !beanConn.equals(currConn)) {
this.bean = loadBean();
if (null == this.bean)
throw new IllegalStateException("EMS bean was null for Resource with type ["
+ this.resourceContext.getResourceType() + "] and key [" + this.resourceContext.getResourceKey()
+ "].");
}
return this.bean;
}
/**
* Sets the MBean that this component considers loaded.
*
* @param bean the new MBean representing the component resource
*/
protected void setEmsBean(EmsBean bean) {
this.bean = bean;
}
public ResourceContext<T> getResourceContext() {
return this.resourceContext;
}
protected void setResourceContext(ResourceContext<T> resourceContext) {
this.resourceContext = resourceContext;
}
/**
* Loads the MBean in a default way. This default mechanism is to look in the
* plugin configuration for a key of {@link #OBJECT_NAME_PROP} and uses that
* as the object name to load via {@link #loadBean(String)}.
*
* Subclasses are free to override this method in order to provide its own
* default loading mechanism.
*
* @return the bean that is loaded
*/
protected EmsBean loadBean() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String objectName = pluginConfig.getSimple(OBJECT_NAME_PROP).getStringValue();
EmsBean loadedBean = loadBean(objectName);
return loadedBean;
}
/**
* Loads the bean with the given object name.
*
* Subclasses are free to override this method in order to load the bean.
*
* @param objectName the name of the bean to load
* @return the bean that is loaded
*/
protected EmsBean loadBean(String objectName) {
EmsConnection emsConnection = getEmsConnection();
if (emsConnection != null) {
EmsBean bean = emsConnection.getBean(objectName);
if (bean == null) {
// In some cases, this resource component may have been discovered by some means other than querying its
// parent's EMSConnection (e.g. ApplicationDiscoveryComponent uses a filesystem to discover EARs and
// WARs that are not yet deployed). In such cases, getBean() will return null, since EMS won't have the
// bean in its cache. To cover such cases, make an attempt to query the underlying MBeanServer for the
// bean before giving up.
emsConnection.queryBeans(objectName);
bean = emsConnection.getBean(objectName);
}
return bean;
}
return null;
}
/**
* Is this service alive?
*
* @return true if the service is running
*/
public AvailabilityType getAvailability() {
try {
return isMBeanAvailable() ? AvailabilityType.UP : AvailabilityType.DOWN;
} catch (RuntimeException e) {
if (this.bean != null) {
// Retry by connecting to a new parent connection (this bean might have been connected to by an old
// provider that's since been recreated).
this.bean = null;
try {
return isMBeanAvailable() ? AvailabilityType.UP : AvailabilityType.DOWN;
} catch (RuntimeException e2) {
if (log.isDebugEnabled() ) {
log.debug("Avail check retry failed, MBean not available", e2);
}
return AvailabilityType.DOWN;
}
} else {
if (log.isDebugEnabled() ) {
log.debug("Avail check failed, MBean not available", e);
}
return AvailabilityType.DOWN;
}
}
}
private boolean isMBeanAvailable() {
EmsBean emsBean = getEmsBean();
boolean isAvailable = emsBean.isRegistered();
if (isAvailable == false) {
// in some buggy situations, a remote server might tell us an MBean isn't registered but it really is.
// see JBPAPP-2031 for more
String emsBeanName = emsBean.getBeanName().getCanonicalName();
int size = emsBean.getConnectionProvider().getExistingConnection().queryBeans(emsBeanName).size();
isAvailable = (size == 1);
}
return isAvailable;
}
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
this.getValues(report, requests, getEmsBean());
}
/**
* Can be called from sub-classes to collect metrics on a bean other than the default bean for this resource
* Supports {a.b} syntax for reading the b Java Bean property from the object value returned from the a jmx
* property. For example,
*
* @param report
* @param requests
* @param bean the EmsBean on which to collect the metrics
*/
protected void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests, EmsBean bean) {
// First we split the requests into their respective beans, and handle calculated values
Set<MeasurementScheduleRequest> defaultBeanRequests = new HashSet<MeasurementScheduleRequest>();
Map<String, Set<MeasurementScheduleRequest>> beansMap = new HashMap<String, Set<MeasurementScheduleRequest>>();
for (MeasurementScheduleRequest request : requests) {
if (getCalculatedProperty(report, request, bean)) {
continue;
}
Matcher m = PROPERTY_PATTERN.matcher(request.getName());
if (m.matches() && (m.group(1) != null)) {
// Custom bean
Set<MeasurementScheduleRequest> props = beansMap.get(m.group(1));
if (props == null) {
props = new HashSet<MeasurementScheduleRequest>();
beansMap.put(m.group(1), props);
}
props.add(request);
} else {
defaultBeanRequests.add(request);
}
}
// First do the default properties against this component's main bean
getBeanProperties(report, bean, defaultBeanRequests);
for (String beanNameTemplate : beansMap.keySet()) {
String transformedbeanName = transformBeanName(beanNameTemplate);
EmsBean otherBean = getEmsConnection().getBean(transformedbeanName);
if (otherBean == null) {
log.info("Unable to retrieve associated MBean: " + transformedbeanName);
} else {
getBeanProperties(report, otherBean, beansMap.get(beanNameTemplate));
}
}
}
private boolean getCalculatedProperty(MeasurementReport report, MeasurementScheduleRequest request, EmsBean bean) {
boolean result = false;
String metricName = request.getName();
if (CALCULATED_METRIC_HEAP_USAGE_PERCENTAGE.equals(request.getName())) {
result = true;
MeasurementReport calculationPropsReport = new MeasurementReport();
Set<MeasurementScheduleRequest> calculationPropsSchedules = new HashSet(2);
MeasurementScheduleRequest heapUsedRequest = new MeasurementScheduleRequest(0, "{HeapMemoryUsage.used}",
0L, true, DataType.MEASUREMENT);
MeasurementScheduleRequest heapComittedRequest = new MeasurementScheduleRequest(0,
"{HeapMemoryUsage.committed}", 0L, true, DataType.MEASUREMENT);
calculationPropsSchedules.add(heapUsedRequest);
calculationPropsSchedules.add(heapComittedRequest);
getBeanProperties(calculationPropsReport, bean, calculationPropsSchedules);
Set<MeasurementDataNumeric> values = calculationPropsReport.getNumericData();
Double heapUsed = Double.NaN;
Double heapCommitted = Double.NaN;
if (null != values && values.size() == 2) {
for (MeasurementDataNumeric v : values) {
if (v.getName().equals("{HeapMemoryUsage.used}")) {
heapUsed = v.getValue();
} else {
heapCommitted = v.getValue();
}
}
}
Double value = Double.NaN;
try {
value = heapUsed / heapCommitted;
} catch (Throwable t) {
// leave as NaN
}
report.addData(new MeasurementDataNumeric(request, value));
}
return result;
}
protected String transformBeanName(String beanTemplate) {
Matcher m = TEMPLATE_PATTERN.matcher(beanTemplate);
while (m.find()) {
String propName = m.group(1);
String replacementValue = resourceContext.getPluginConfiguration().getSimpleValue(propName, null);
beanTemplate = beanTemplate.replaceAll("%" + propName + "%", replacementValue);
m = TEMPLATE_PATTERN.matcher(beanTemplate);
}
return beanTemplate;
}
protected void getBeanProperties(MeasurementReport report, EmsBean thisBean,
Set<MeasurementScheduleRequest> requests) {
List<String> props = new ArrayList<String>();
for (MeasurementScheduleRequest request : requests) {
Matcher m = PROPERTY_PATTERN.matcher(request.getName());
if (m.matches()) {
// Complex property
props.add(getAttributeName(m.group(2)));
} else {
// Simple property
props.add(request.getName());
}
}
List<EmsAttribute> refreshedAttributes = thisBean.refreshAttributes(props);
for (MeasurementScheduleRequest request : requests) {
Matcher m = PROPERTY_PATTERN.matcher(request.getName());
String fullProperty = null;
String attributeName;
if (m.matches()) {
// Complex property
fullProperty = m.group(2);
attributeName = getAttributeName(fullProperty);
} else {
attributeName = request.getName();
}
EmsAttribute attribute = null;
for (EmsAttribute refreshedAttribute : refreshedAttributes) {
if (attributeName.equals(refreshedAttribute.getName())) {
attribute = refreshedAttribute;
}
}
if (attribute == null) {
log.debug("Unable to collect measurement, attribute [" + request.getName() + "] not found on ["
+ this.resourceContext.getResourceKey() + "]");
// TODO GH: report.addError
} else {
Object value = attribute.getValue();
if ((value != null) && (fullProperty != null)) {
// we're meant to load a specific property of the returned value object
value = lookupAttributeProperty(value, fullProperty);
}
if (request.getDataType() == DataType.MEASUREMENT) {
if (value instanceof Number) {
report.addData(new MeasurementDataNumeric(request, ((Number) value).doubleValue()));
} else if ((value instanceof List<?>)) {
// add the number of elements
report.addData(new MeasurementDataNumeric(request, Double.valueOf(((List<?>) value).size())));
}
} else if (request.getDataType() == DataType.TRAIT) {
String displayValue = null;
if ((value != null) && value.getClass().isArray()) {
displayValue = Arrays.deepToString((Object[]) value);
} else {
displayValue = String.valueOf(value);
}
report.addData(new MeasurementDataTrait(request, displayValue));
}
}
}
}
protected Object lookupAttributeProperty(Object value, String property) {
String[] ps = property.split("\\.", 2);
String searchProperty = ps[0];
// Values returned from EMS connections may be from JMX classes loaded in separate classloaders (for server compatibility)
// so we use reflection to be able to handle any instance of the CompositeData class.
Class[] interfaces = value.getClass().getInterfaces();
boolean isCompositeData = false;
for (Class intf : interfaces) {
if (intf.getName().equals(CompositeData.class.getName())) {
isCompositeData = true;
}
}
if (value.getClass().getName().equals(CompositeData.class.getName()) || isCompositeData) {
try {
Method m = value.getClass().getMethod("get", String.class);
value = m.invoke(value, ps[1]);
} catch (NoSuchMethodException e) {
/* Won't happen */
} catch (Exception e) {
log.info("Unable to read attribute property [" + property + "] from composite data value", e);
}
} else {
// Try to use reflection
try {
PropertyDescriptor[] pds = Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
if (pd.getName().equals(searchProperty)) {
value = pd.getReadMethod().invoke(value);
}
}
} catch (Exception e) {
log.debug("Unable to read property from measurement attribute [" + searchProperty + "] not found on ["
+ this.resourceContext.getResourceKey() + "]");
}
}
if (ps.length > 1) {
value = lookupAttributeProperty(value, ps[1]);
}
return value;
}
protected String getAttributeName(String property) {
return property.split("\\.", 2)[0];
}
protected String getAttributeProperty(String property) {
if (property.startsWith("{")) {
return property.substring(property.indexOf('.') + 1, property.length() - 1);
} else {
return null;
}
}
/**
* This default setup of configuration properties can map to mbean attributes
*
* @return the configuration of the component
*/
public Configuration loadResourceConfiguration() {
Configuration configuration = new Configuration();
ConfigurationDefinition configurationDefinition = this.resourceContext.getResourceType()
.getResourceConfigurationDefinition();
for (PropertyDefinition property : configurationDefinition.getPropertyDefinitions().values()) {
if (property instanceof PropertyDefinitionSimple) {
EmsAttribute attribute = getEmsBean().getAttribute(property.getName());
if (attribute != null) {
configuration.put(new PropertySimple(property.getName(), attribute.refresh()));
}
}
}
return configuration;
}
/**
* Equivalent to updateResourceConfiguration(report, false);
*/
public void updateResourceConfiguration(ConfigurationUpdateReport report) {
updateResourceConfiguration(report, false);
}
public void updateResourceConfiguration(ConfigurationUpdateReport report, boolean ignoreReadOnly) {
ConfigurationDefinition configurationDefinition = this.getResourceContext().getResourceType()
.getResourceConfigurationDefinition();
// assume we succeed - we'll set to failure if we can't set all properties
report.setStatus(ConfigurationUpdateStatus.SUCCESS);
for (String key : report.getConfiguration().getSimpleProperties().keySet()) {
PropertySimple property = report.getConfiguration().getSimple(key);
if (property != null) {
EmsAttribute attribute = this.bean.getAttribute(key);
try {
PropertyDefinitionSimple def = configurationDefinition.getPropertyDefinitionSimple(property
.getName());
if (!(ignoreReadOnly && def.isReadOnly())) {
switch (def.getType()) {
case INTEGER: {
attribute.setValue(property.getIntegerValue());
break;
}
case LONG: {
attribute.setValue(property.getLongValue());
break;
}
case BOOLEAN: {
attribute.setValue(property.getBooleanValue());
break;
}
case FLOAT: {
attribute.setValue(property.getFloatValue());
break;
}
case DOUBLE: {
attribute.setValue(property.getDoubleValue());
break;
}
default: {
attribute.setValue(property.getStringValue());
break;
}
}
}
} catch (Exception e) {
property.setErrorMessage(ThrowableUtil.getStackAsString(e));
report
.setErrorMessage("Failed setting resource configuration - see property error messages for details");
log.info("Failure setting MBean Resource configuration value for " + key, e);
}
}
}
}
public EmsConnection getEmsConnection() {
return this.resourceContext.getParentResourceComponent().getEmsConnection();
}
public OperationResult invokeOperation(String name, Configuration parameters) throws Exception {
return invokeOperation(name, parameters, getEmsBean());
}
public OperationResult invokeOperation(String name, Configuration parameters, EmsBean emsBean) throws Exception {
if (emsBean == null) {
throw new Exception("Can not invoke operation [" + name
+ "], as we can't connect to the MBean - is it down?");
}
Map<String, PropertySimple> paramProps = parameters.getSimpleProperties();
SortedSet<EmsOperation> emsOperations = emsBean.getOperations();
EmsOperation operation = null;
// There could be multiple operations with the same name but different parameters. Try to find one that has
// the same # of parameters as the RHQ operation def.
for (EmsOperation emsOperation : emsOperations) {
if (emsOperation.getName().equals(name) && (emsOperation.getParameters().size() == paramProps.size())) {
operation = emsOperation;
break;
}
}
if (operation == null) {
// We couldn't find an operation with the expected name and # of parameters, so as as a last ditch effort,
// see if there's an operation that at least has the expected name.
operation = emsBean.getOperation(name);
}
if (operation == null) {
throw new Exception("Operation [" + name + "] not found on MBean [" + emsBean.getBeanName() + "].");
}
List<EmsParameter> emsParams = operation.getParameters();
Map<String, Integer> emsParamIndexesByName = new HashMap<String, Integer>();
for (int i = 0, emsParamsSize = emsParams.size(); i < emsParamsSize; i++) {
EmsParameter emsParam = emsParams.get(i);
if (emsParam.getName() != null) {
emsParamIndexesByName.put(emsParam.getName(), i);
}
}
Object[] paramValues = new Object[operation.getParameters().size()];
for (String propName : paramProps.keySet()) {
Integer paramIndex;
if (propName.matches("\\[\\d+\\]")) {
paramIndex = Integer.valueOf(propName.substring(propName.indexOf('[') + 1, propName.indexOf(']')));
if (paramIndex < 0 || paramIndex >= emsParams.size()) {
throw new IllegalStateException("Index [" + paramIndex + "] specified for parameter of operation ["
+ name + "] on MBean [" + emsBean.getBeanName() + "] is invalid. The MBean operation takes "
+ emsParams.size() + " parameters.");
}
} else {
paramIndex = emsParamIndexesByName.get(propName);
if (paramIndex == null) {
throw new IllegalStateException("Name [" + propName + "] specified for parameter of operation ["
+ name + "] on MBean [" + emsBean.getBeanName()
+ "] is invalid. The MBean operation does not take a parameter by that name.");
}
}
EmsParameter emsParam = emsParams.get(paramIndex);
PropertySimple paramProp = paramProps.get(propName);
String emsParamType = emsParam.getType();
Object paramValue = getPropertyValueAsType(paramProp, emsParamType);
paramValues[paramIndex] = paramValue;
}
Object resultObject = operation.invoke(paramValues);
boolean hasVoidReturnType = (operation.getReturnType() == null
|| Void.class.getName().equals(operation.getReturnType()) || void.class.getName().equals(
operation.getReturnType()));
OperationResult resultToReturn;
if (resultObject == null && hasVoidReturnType) {
resultToReturn = null;
} else {
// if the returned object is an array, put the elements in a list so we can stringify it later
if (resultObject != null && resultObject.getClass().isArray()) {
int len = Array.getLength(resultObject);
ArrayList<Object> list = new ArrayList<Object>(len);
for (int index = 0; index < len; index++) {
list.add(Array.get(resultObject, index));
}
resultObject = list;
}
// put the results object in an operation result if it isn't already one
if (resultObject instanceof OperationResult) {
resultToReturn = (OperationResult) resultObject;
} else {
resultToReturn = new OperationResult(String.valueOf(resultObject));
}
}
return resultToReturn;
}
protected Object getPropertyValueAsType(PropertySimple propSimple, String typeName) {
Object value;
if (typeName.equals(String.class.getName())) {
value = (propSimple == null) ? null : propSimple.getStringValue();
} else if (typeName.equals(Boolean.class.getName()) || typeName.equals(boolean.class.getName())) {
value = (propSimple == null) ? null : propSimple.getBooleanValue();
} else if (typeName.equals(Integer.class.getName()) || typeName.equals(int.class.getName())) {
value = (propSimple == null) ? null : propSimple.getIntegerValue();
} else if (typeName.equals(Long.class.getName()) || typeName.equals(long.class.getName())) {
value = (propSimple == null) ? null : propSimple.getLongValue();
} else if (typeName.equals(Float.class.getName()) || typeName.equals(float.class.getName())) {
value = (propSimple == null) ? null : propSimple.getFloatValue();
} else if (typeName.equals(Double.class.getName()) || typeName.equals(double.class.getName())) {
value = (propSimple == null) ? null : propSimple.getDoubleValue();
} else {
throw new IllegalStateException("Operation parameter maps to MBean parameter with an unsupported type ("
+ typeName + ").");
}
// TODO GH: Handle rest of types. (I think i have a mapper for this in mc4j
return value;
}
}