/* * Licensed to ElasticSearch and Shay Banon under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. ElasticSearch 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.elasticsearch.jmx; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.elasticsearch.common.Classes; import org.elasticsearch.common.Preconditions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.logging.ESLogger; import javax.management.*; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.elasticsearch.common.collect.MapBuilder.newMapBuilder; /** * */ public class ResourceDMBean implements DynamicMBean { private static final Class<?>[] primitives = {int.class, byte.class, short.class, long.class, float.class, double.class, boolean.class, char.class}; private final ESLogger logger; private final Object obj; private final String objectName; private final String groupName; private final String fullObjectName; private final String description; private final MBeanAttributeInfo[] attributesInfo; private final MBeanOperationInfo[] operationsInfo; private final MBeanInfo mBeanInfo; private final ImmutableMap<String, AttributeEntry> attributes; private final ImmutableList<MBeanOperationInfo> operations; public ResourceDMBean(Object instance, ESLogger logger) { Preconditions.checkNotNull(instance, "Cannot make an MBean wrapper for null instance"); this.obj = instance; this.logger = logger; MapBuilder<String, AttributeEntry> attributesBuilder = newMapBuilder(); List<MBeanOperationInfo> operationsBuilder = new ArrayList<MBeanOperationInfo>(); MBean mBean = obj.getClass().getAnnotation(MBean.class); this.groupName = findGroupName(); if (mBean != null && Strings.hasLength(mBean.objectName())) { objectName = mBean.objectName(); } else { if (Strings.hasLength(groupName)) { // we have something in the group object name, don't put anything in the object name objectName = ""; } else { objectName = obj.getClass().getSimpleName(); } } StringBuilder sb = new StringBuilder(groupName); if (Strings.hasLength(groupName) && Strings.hasLength(objectName)) { sb.append(","); } sb.append(objectName); this.fullObjectName = sb.toString(); this.description = findDescription(); findFields(attributesBuilder); findMethods(attributesBuilder, operationsBuilder); this.attributes = attributesBuilder.immutableMap(); this.operations = ImmutableList.copyOf(operationsBuilder); attributesInfo = new MBeanAttributeInfo[attributes.size()]; int i = 0; MBeanAttributeInfo info; for (AttributeEntry entry : attributes.values()) { info = entry.getInfo(); attributesInfo[i++] = info; if (logger.isInfoEnabled()) { logger.trace("Attribute " + info.getName() + "[r=" + info.isReadable() + ",w=" + info.isWritable() + ",is=" + info.isIs() + ",type=" + info.getType() + "]"); } } operationsInfo = new MBeanOperationInfo[operations.size()]; operations.toArray(operationsInfo); if (logger.isTraceEnabled()) { if (operations.size() > 0) logger.trace("Operations are:"); for (MBeanOperationInfo op : operationsInfo) { logger.trace("Operation " + op.getReturnType() + " " + op.getName()); } } this.mBeanInfo = new MBeanInfo(getObject().getClass().getCanonicalName(), description, attributesInfo, null, operationsInfo, null); } public MBeanInfo getMBeanInfo() { return mBeanInfo; } public synchronized Object getAttribute(String name) throws AttributeNotFoundException { if (name == null || name.length() == 0) throw new NullPointerException("Invalid attribute requested " + name); Attribute attr = getNamedAttribute(name); if (attr == null) { throw new AttributeNotFoundException("Unknown attribute '" + name + "'. Known attributes names are: " + attributes.keySet()); } return attr.getValue(); } public synchronized void setAttribute(Attribute attribute) { if (attribute == null || attribute.getName() == null) throw new NullPointerException("Invalid attribute requested " + attribute); setNamedAttribute(attribute); } public synchronized AttributeList getAttributes(String[] names) { AttributeList al = new AttributeList(); for (String name : names) { Attribute attr = getNamedAttribute(name); if (attr != null) { al.add(attr); } else { logger.warn("Did not find attribute " + name); } } return al; } public synchronized AttributeList setAttributes(AttributeList list) { AttributeList results = new AttributeList(); for (Object aList : list) { Attribute attr = (Attribute) aList; if (setNamedAttribute(attr)) { results.add(attr); } else { if (logger.isWarnEnabled()) { logger.warn("Failed to update attribute name " + attr.getName() + " with value " + attr.getValue()); } } } return results; } public Object invoke(String name, Object[] args, String[] sig) throws MBeanException, ReflectionException { if (logger.isDebugEnabled()) { logger.debug("Invoke method called on " + name); } MBeanOperationInfo opInfo = null; for (MBeanOperationInfo op : operationsInfo) { if (op.getName().equals(name)) { opInfo = op; break; } } if (opInfo == null) { final String msg = "Operation " + name + " not in ModelMBeanInfo"; throw new MBeanException(new ServiceNotFoundException(msg), msg); } try { Class<?>[] classes = new Class[sig.length]; for (int i = 0; i < classes.length; i++) { classes[i] = getClassForName(sig[i]); } Method method = getObject().getClass().getMethod(name, classes); return method.invoke(getObject(), args); } catch (Exception e) { throw new MBeanException(e); } } Object getObject() { return obj; } private Class<?> getClassForName(String name) throws ClassNotFoundException { try { return Classes.getDefaultClassLoader().loadClass(name); } catch (ClassNotFoundException cnfe) { // Could be a primitive - let's check for (Class<?> primitive : primitives) { if (name.equals(primitive.getName())) { return primitive; } } } throw new ClassNotFoundException("Class " + name + " cannot be found"); } private String findGroupName() { Class objClass = getObject().getClass(); while (objClass != Object.class) { Method[] methods = objClass.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(ManagedGroupName.class)) { try { method.setAccessible(true); return (String) method.invoke(getObject()); } catch (Exception e) { logger.warn("Failed to get group name for [" + getObject() + "]", e); } } } objClass = objClass.getSuperclass(); } return ""; } private String findDescription() { MBean mbean = getObject().getClass().getAnnotation(MBean.class); if (mbean != null && mbean.description() != null && mbean.description().trim().length() > 0) { return mbean.description(); } return ""; } private void findMethods(MapBuilder<String, AttributeEntry> attributesBuilder, List<MBeanOperationInfo> ops) { // find all methods but don't include methods from Object class List<Method> methods = new ArrayList<Method>(Arrays.asList(getObject().getClass().getMethods())); List<Method> objectMethods = new ArrayList<Method>(Arrays.asList(Object.class.getMethods())); methods.removeAll(objectMethods); for (Method method : methods) { // does method have @ManagedAttribute annotation? ManagedAttribute attr = method.getAnnotation(ManagedAttribute.class); if (attr != null) { String methodName = method.getName(); if (!methodName.startsWith("get") && !methodName.startsWith("set") && !methodName.startsWith("is")) { if (logger.isWarnEnabled()) logger.warn("method name " + methodName + " doesn't start with \"get\", \"set\", or \"is\"" + ", but is annotated with @ManagedAttribute: will be ignored"); } else { MBeanAttributeInfo info; String attributeName = null; boolean writeAttribute = false; if (isSetMethod(method)) { // setter attributeName = methodName.substring(3); info = new MBeanAttributeInfo(attributeName, method.getParameterTypes()[0] .getCanonicalName(), attr.description(), true, true, false); writeAttribute = true; } else { // getter if (method.getParameterTypes().length == 0 && method.getReturnType() != java.lang.Void.TYPE) { boolean hasSetter = attributesBuilder.containsKey(attributeName); // we found is method if (methodName.startsWith("is")) { attributeName = methodName.substring(2); info = new MBeanAttributeInfo(attributeName, method.getReturnType() .getCanonicalName(), attr.description(), true, hasSetter, true); } else { // this has to be get attributeName = methodName.substring(3); info = new MBeanAttributeInfo(attributeName, method.getReturnType() .getCanonicalName(), attr.description(), true, hasSetter, false); } } else { if (logger.isWarnEnabled()) { logger.warn("Method " + method.getName() + " must have a valid return type and zero parameters"); } continue; } } AttributeEntry ae = attributesBuilder.get(attributeName); // is it a read method? if (!writeAttribute) { // we already have annotated field as read if (ae instanceof FieldAttributeEntry && ae.getInfo().isReadable()) { logger.warn("not adding annotated method " + method + " since we already have read attribute"); } // we already have annotated set method else if (ae instanceof MethodAttributeEntry) { MethodAttributeEntry mae = (MethodAttributeEntry) ae; if (mae.hasSetMethod()) { attributesBuilder.put(attributeName, new MethodAttributeEntry(mae.getInfo(), mae .getSetMethod(), method)); } } // we don't have such entry else { attributesBuilder.put(attributeName, new MethodAttributeEntry(info, null, method)); } }// is it a set method? else { if (ae instanceof FieldAttributeEntry) { // we already have annotated field as write if (ae.getInfo().isWritable()) { logger.warn("Not adding annotated method " + methodName + " since we already have writable attribute"); } else { // we already have annotated field as read // lets make the field writable Field f = ((FieldAttributeEntry) ae).getField(); MBeanAttributeInfo i = new MBeanAttributeInfo(ae.getInfo().getName(), f.getType().getCanonicalName(), attr.description(), true, !Modifier.isFinal(f.getModifiers()), false); attributesBuilder.put(attributeName, new FieldAttributeEntry(i, f)); } } // we already have annotated getOrIs method else if (ae instanceof MethodAttributeEntry) { MethodAttributeEntry mae = (MethodAttributeEntry) ae; if (mae.hasIsOrGetMethod()) { attributesBuilder.put(attributeName, new MethodAttributeEntry(info, method, mae .getIsOrGetMethod())); } } // we don't have such entry else { attributesBuilder.put(attributeName, new MethodAttributeEntry(info, method, null)); } } } } else if (method.isAnnotationPresent(ManagedOperation.class)) { ManagedOperation op = method.getAnnotation(ManagedOperation.class); String attName = method.getName(); if (isSetMethod(method) || isGetMethod(method)) { attName = attName.substring(3); } else if (isIsMethod(method)) { attName = attName.substring(2); } // expose unless we already exposed matching attribute field boolean isAlreadyExposed = attributesBuilder.containsKey(attName); if (!isAlreadyExposed) { ops.add(new MBeanOperationInfo(op != null ? op.description() : "", method)); } } } } private boolean isSetMethod(Method method) { return (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && method.getReturnType() == java.lang.Void.TYPE); } private boolean isGetMethod(Method method) { return (method.getParameterTypes().length == 0 && method.getReturnType() != java.lang.Void.TYPE && method.getName().startsWith("get")); } private boolean isIsMethod(Method method) { return (method.getParameterTypes().length == 0 && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class) && method.getName().startsWith("is")); } private void findFields(MapBuilder<String, AttributeEntry> attributesBuilder) { // traverse class hierarchy and find all annotated fields for (Class<?> clazz = getObject().getClass(); clazz != null; clazz = clazz.getSuperclass()) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { ManagedAttribute attr = field.getAnnotation(ManagedAttribute.class); if (attr != null) { String fieldName = renameToJavaCodingConvention(field.getName()); MBeanAttributeInfo info = new MBeanAttributeInfo(fieldName, field.getType().getCanonicalName(), attr.description(), true, !Modifier.isFinal(field.getModifiers()) && attr.writable(), false); attributesBuilder.put(fieldName, new FieldAttributeEntry(info, field)); } } } } private Attribute getNamedAttribute(String name) { Attribute result = null; AttributeEntry entry = attributes.get(name); if (entry != null) { MBeanAttributeInfo i = entry.getInfo(); try { result = new Attribute(name, entry.invoke(null)); if (logger.isDebugEnabled()) logger.debug("Attribute " + name + " has r=" + i.isReadable() + ",w=" + i.isWritable() + ",is=" + i.isIs() + " and value " + result.getValue()); } catch (Exception e) { logger.debug("Exception while reading value of attribute " + name, e); } } else { logger.warn("Did not find queried attribute with name " + name); } return result; } private boolean setNamedAttribute(Attribute attribute) { boolean result = false; if (logger.isDebugEnabled()) logger.debug("Invoking set on attribute " + attribute.getName() + " with value " + attribute.getValue()); AttributeEntry entry = attributes.get(attribute.getName()); if (entry != null) { try { entry.invoke(attribute); result = true; } catch (Exception e) { logger.warn("Exception while writing value for attribute " + attribute.getName(), e); } } else { logger.warn("Could not invoke set on attribute " + attribute.getName() + " with value " + attribute.getValue()); } return result; } private String renameToJavaCodingConvention(String fieldName) { if (fieldName.contains("_")) { Pattern p = Pattern.compile("_."); Matcher m = p.matcher(fieldName); StringBuffer sb = new StringBuffer(); while (m.find()) { m.appendReplacement(sb, fieldName.substring(m.end() - 1, m.end()).toUpperCase()); } m.appendTail(sb); char first = sb.charAt(0); if (Character.isLowerCase(first)) { sb.setCharAt(0, Character.toUpperCase(first)); } return sb.toString(); } else { if (Character.isLowerCase(fieldName.charAt(0))) { return fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); } else { return fieldName; } } } private class MethodAttributeEntry implements AttributeEntry { final MBeanAttributeInfo info; final Method isOrGetmethod; final Method setMethod; public MethodAttributeEntry(final MBeanAttributeInfo info, final Method setMethod, final Method isOrGetMethod) { super(); this.info = info; this.setMethod = setMethod; this.isOrGetmethod = isOrGetMethod; } public Object invoke(Attribute a) throws Exception { if (a == null && isOrGetmethod != null) return isOrGetmethod.invoke(getObject()); else if (a != null && setMethod != null) return setMethod.invoke(getObject(), a.getValue()); else return null; } public MBeanAttributeInfo getInfo() { return info; } public boolean hasIsOrGetMethod() { return isOrGetmethod != null; } public boolean hasSetMethod() { return setMethod != null; } public Method getIsOrGetMethod() { return isOrGetmethod; } public Method getSetMethod() { return setMethod; } } private class FieldAttributeEntry implements AttributeEntry { private final MBeanAttributeInfo info; private final Field field; public FieldAttributeEntry(final MBeanAttributeInfo info, final Field field) { super(); this.info = info; this.field = field; if (!field.isAccessible()) { field.setAccessible(true); } } public Field getField() { return field; } public Object invoke(Attribute a) throws Exception { if (a == null) { return field.get(getObject()); } else { field.set(getObject(), a.getValue()); return null; } } public MBeanAttributeInfo getInfo() { return info; } } private interface AttributeEntry { public Object invoke(Attribute a) throws Exception; public MBeanAttributeInfo getInfo(); } public boolean isManagedResource() { return !attributes.isEmpty() || !operations.isEmpty(); } public String getFullObjectName() { return this.fullObjectName; } public String getObjectName() { return this.objectName; } public String getGroupName() { return this.groupName; } }