001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.jmx;
022    
023    import java.beans.BeanInfo;
024    import java.beans.Introspector;
025    import java.beans.MethodDescriptor;
026    import java.beans.PropertyDescriptor;
027    import java.lang.annotation.Annotation;
028    import java.lang.reflect.Method;
029    import java.math.BigDecimal;
030    import java.math.BigInteger;
031    import java.util.ArrayList;
032    import java.util.Arrays;
033    import java.util.Date;
034    import java.util.HashMap;
035    import java.util.HashSet;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.Set;
039    
040    import javax.management.Attribute;
041    import javax.management.AttributeList;
042    import javax.management.AttributeNotFoundException;
043    import javax.management.DynamicMBean;
044    import javax.management.InvalidAttributeValueException;
045    import javax.management.MBeanException;
046    import javax.management.MBeanInfo;
047    import javax.management.MBeanNotificationInfo;
048    import javax.management.MBeanOperationInfo;
049    import javax.management.ObjectName;
050    import javax.management.ReflectionException;
051    import javax.management.openmbean.ArrayType;
052    import javax.management.openmbean.OpenDataException;
053    import javax.management.openmbean.OpenMBeanAttributeInfo;
054    import javax.management.openmbean.OpenMBeanAttributeInfoSupport;
055    import javax.management.openmbean.OpenMBeanConstructorInfo;
056    import javax.management.openmbean.OpenMBeanInfoSupport;
057    import javax.management.openmbean.OpenMBeanOperationInfo;
058    import javax.management.openmbean.OpenMBeanOperationInfoSupport;
059    import javax.management.openmbean.OpenMBeanParameterInfo;
060    import javax.management.openmbean.OpenMBeanParameterInfoSupport;
061    import javax.management.openmbean.OpenType;
062    import javax.management.openmbean.SimpleType;
063    
064    /**
065     * The OpenMBean class wraps an instance of any bean and introspects its properties and methods
066     * for MBeanAttributes and MBeanOperations. It implements all functionalities required by a
067     * {@link DynamicMBean} and returns an OpenMBeanInfo object.
068     * <br>
069     * <br>
070     * Limitations:
071     * <ul>
072     * <li>only attributes and operations are supported (no contructor and no notification),</li>
073     * <li>only {@link SimpleType} and {@link ArrayType} are supported (no composite type).</li>
074     * </ul>
075     * 
076     * @author Franck WOLFF
077     */
078    @SuppressWarnings({ "unchecked", "rawtypes" })
079    public class OpenMBean implements DynamicMBean {
080    
081            ///////////////////////////////////////////////////////////////////////////
082            // Fields.
083    
084            private static final Map<Class<?>, SimpleType> SIMPLE_TYPES = new HashMap<Class<?>, SimpleType>();
085            static {
086                    SIMPLE_TYPES.put(Void.class, SimpleType.VOID);
087                    SIMPLE_TYPES.put(Void.TYPE, SimpleType.VOID);
088                    SIMPLE_TYPES.put(Boolean.class, SimpleType.BOOLEAN);
089                    SIMPLE_TYPES.put(Boolean.TYPE, SimpleType.BOOLEAN);
090                    SIMPLE_TYPES.put(Character.class, SimpleType.CHARACTER);
091                    SIMPLE_TYPES.put(Character.TYPE, SimpleType.CHARACTER);
092                    SIMPLE_TYPES.put(Byte.class, SimpleType.BYTE);
093                    SIMPLE_TYPES.put(Byte.TYPE, SimpleType.BYTE);
094                    SIMPLE_TYPES.put(Short.class, SimpleType.SHORT);
095                    SIMPLE_TYPES.put(Short.TYPE, SimpleType.SHORT);
096                    SIMPLE_TYPES.put(Integer.class, SimpleType.INTEGER);
097                    SIMPLE_TYPES.put(Integer.TYPE, SimpleType.INTEGER);
098                    SIMPLE_TYPES.put(Long.class, SimpleType.LONG);
099                    SIMPLE_TYPES.put(Long.TYPE, SimpleType.LONG);
100                    SIMPLE_TYPES.put(Float.class, SimpleType.FLOAT);
101                    SIMPLE_TYPES.put(Float.TYPE, SimpleType.FLOAT);
102                    SIMPLE_TYPES.put(Double.class, SimpleType.DOUBLE);
103                    SIMPLE_TYPES.put(Double.TYPE, SimpleType.DOUBLE);
104                    SIMPLE_TYPES.put(String.class, SimpleType.STRING);
105                    SIMPLE_TYPES.put(BigDecimal.class, SimpleType.BIGDECIMAL);
106                    SIMPLE_TYPES.put(BigInteger.class, SimpleType.BIGINTEGER);
107                    SIMPLE_TYPES.put(Date.class, SimpleType.DATE);
108                    SIMPLE_TYPES.put(ObjectName.class, SimpleType.OBJECTNAME);
109            }
110            
111            private final MBeanInfo info;
112            private final Object instance;
113            
114            private final Map<String, PropertyDescriptor> attributesMap = new HashMap<String, PropertyDescriptor>();
115            private final Map<String, MethodDescriptor> operationsMap = new HashMap<String, MethodDescriptor>();
116    
117            ///////////////////////////////////////////////////////////////////////////
118            // Constructors.
119            
120            /**
121             * Creates a new OpenMBean instance and instrospects its child class for attributes
122             * and operations.
123             */
124            protected OpenMBean() {
125                    this.info = init(getClass(), OpenMBean.class, attributesMap, operationsMap);
126                    this.instance = this;
127            }
128            
129            private OpenMBean(Class<?> beanClass, Class<?> stopClass, Object instance) {
130                    this.info = init(beanClass, stopClass, attributesMap, operationsMap);
131                    this.instance = instance;
132            }
133    
134            ///////////////////////////////////////////////////////////////////////////
135            // Static OpenMBean creators.
136    
137            /**
138             * Creates a new OpenMBean by introspecting and wrapping the <tt>instance</tt> parameter.
139             * This method search for an interface named <tt>instance.getClass().getSimpleName() + "MBean"</tt>
140             * and, if it finds it, uses it for introspection. Otherwise, the class of the <tt>instance</tt>
141             * object is used instead.
142             * 
143             * @param instance an instance of a bean to introspect.
144             * @return a new OpenMBean instance that wraps the instance bean.
145             */
146            public static OpenMBean createMBean(Object instance) {
147                    
148                    Class<?> beanClass = null;
149                    Class<?> instanceClass = instance.getClass();
150                    while (instanceClass != null) {
151                            String interMBeanName = instanceClass.getSimpleName() + "MBean";
152                            for (Class<?> inter : instanceClass.getInterfaces()) {
153                                    if (interMBeanName.equals(inter.getSimpleName())) {
154                                            beanClass = inter;
155                                            break;
156                                    }
157                            }
158                            if (beanClass != null)
159                                    break;
160                            instanceClass = instanceClass.getSuperclass();
161                    }
162                    
163                    if (beanClass == null)
164                            beanClass = instance.getClass();
165                    
166                    Class<?> stopClass = null;
167                    if (!beanClass.isInterface()) {
168                            stopClass = beanClass.getSuperclass();
169                            if (stopClass == null)
170                                    stopClass = Object.class;
171                    }
172                    
173                    return new OpenMBean(beanClass, stopClass, instance);
174            }
175            
176            /**
177             * Creates a new OpenMBean by introspecting the <tt>beanClass</tt> parameter and wrapping
178             * the <tt>instance</tt> parameter.
179             * 
180             * @param beanClass a class (or interface) used for introspection.
181             * @param instance the bean to encapsulate.
182             * @return a new OpenMBean instance that wraps the instance bean.
183             * @throws IllegalArgumentException if instance is not an instance of beanClass.
184             */
185            public static OpenMBean createMBean(Class<?> beanClass, Object instance) {
186                    if (!beanClass.isAssignableFrom(instance.getClass()))
187                            throw new IllegalArgumentException("Instance " + instance + " should be an instance of " + beanClass);
188    
189                    Class<?> stopClass = null;
190                    if (!beanClass.isInterface()) {
191                            stopClass = beanClass.getSuperclass();
192                            if (stopClass == null)
193                                    stopClass = Object.class;
194                    }
195    
196                    return new OpenMBean(beanClass, stopClass, instance);
197            }
198    
199            ///////////////////////////////////////////////////////////////////////////
200            // Static initialization methods.
201            
202            private static MBeanInfo init(
203                    Class<?> beanClass,
204                    Class<?> stopClass,
205                    Map<String, PropertyDescriptor> attributesMap,
206                    Map<String, MethodDescriptor> operationsMap) {
207                    
208                    MBean mb = beanClass.getAnnotation(MBean.class);
209                    
210                    String description = null;
211                    if (mb != null)
212                            description = mb.description();
213                    if (description == null)
214                            description = beanClass.getSimpleName() + " MBean";
215                    
216                    List<OpenMBeanAttributeInfo> attributes = new ArrayList<OpenMBeanAttributeInfo>();
217                    List<OpenMBeanOperationInfo> operations = new ArrayList<OpenMBeanOperationInfo>();
218                    
219                    try {
220                            BeanInfo beanInfo = (
221                                    stopClass == null ?
222                                    Introspector.getBeanInfo(beanClass) :
223                                    Introspector.getBeanInfo(beanClass, stopClass)
224                            );
225                            
226                            Set<Method> attributeMethods = new HashSet<Method>();
227                            
228                            // Attributes.
229                            for (PropertyDescriptor property : beanInfo.getPropertyDescriptors()) {
230    
231                                    MBeanAttribute mba = null;
232                                    if (property.getReadMethod() != null)
233                                            mba = property.getReadMethod().getAnnotation(MBeanAttribute.class);
234                                    if (mba == null && property.getWriteMethod() != null)
235                                            mba = property.getWriteMethod().getAnnotation(MBeanAttribute.class);
236                                    
237                                    if (mba == null)
238                                            continue;
239                                    
240                                    String name = property.getName();
241                                    
242                                    String desc = mba.description();
243                                    if (desc == null)
244                                            desc = name.substring(0, 1).toUpperCase() + name.substring(1) + " Attribute";
245                                    
246                                    OpenType type = getOpenType(property.getPropertyType());
247                                    
248                                    attributes.add(new OpenMBeanAttributeInfoSupport(
249                                            name,
250                                            desc,
251                                            type,
252                                            property.getReadMethod() != null,
253                                            property.getWriteMethod() != null,
254                                            property.getReadMethod() != null && property.getReadMethod().getName().startsWith("is")
255                                    ));
256                                    attributesMap.put(property.getName(), property);
257                                    
258                                    if (property.getReadMethod() != null)
259                                            attributeMethods.add(property.getReadMethod());
260                                    if (property.getWriteMethod() != null)
261                                            attributeMethods.add(property.getWriteMethod());
262                            }
263                            
264                            // Operations
265                            for (MethodDescriptor method : beanInfo.getMethodDescriptors()) {
266                                    
267                                    if (attributeMethods.contains(method.getMethod()))
268                                            continue;
269                                    
270                                    MBeanOperation mbo = method.getMethod().getAnnotation(MBeanOperation.class);
271                                    
272                                    if (mbo == null)
273                                            continue;
274                                    
275                                    String name = method.getName();
276    
277                                    String desc = mbo.description();
278                                    if (desc == null)
279                                            desc = name.substring(0, 1).toUpperCase() + name.substring(1) + " Operation";
280                                    
281                                    List<OpenMBeanParameterInfo> parameters = new ArrayList<OpenMBeanParameterInfo>();
282                                    Annotation[][] annotations = method.getMethod().getParameterAnnotations();
283                                    
284                                    int i = 0;
285                                    for (Class<?> parameter : method.getMethod().getParameterTypes()) {
286                                            String paramName = getParameterName(annotations, i);
287                                            String paramDesc = getParameterDescription(annotations, i);
288                                            OpenType paramType = getOpenType(parameter);
289                                            
290                                            parameters.add(new OpenMBeanParameterInfoSupport(paramName, paramDesc, paramType));
291                                            
292                                            i++;
293                                    }
294                                    
295                                    OpenType returnedType = getOpenType(method.getMethod().getReturnType());
296                                    
297                                    int impact = MBeanOperationInfo.UNKNOWN;
298                                    if (mbo.impact() != null) {
299                                            switch (mbo.impact()) {
300                                                    case ACTION: impact = MBeanOperationInfo.ACTION; break;
301                                                    case ACTION_INFO: impact = MBeanOperationInfo.ACTION_INFO; break;
302                                                    case INFO: impact = MBeanOperationInfo.INFO; break;
303                                            }
304                                    }
305                                    
306                                    operations.add(new OpenMBeanOperationInfoSupport(
307                                            name,
308                                            desc,
309                                            parameters.toArray(new OpenMBeanParameterInfo[parameters.size()]),
310                                            returnedType,
311                                            impact
312                                    ));
313                                    
314                                    String[] paramClasses = new String[parameters.size()];
315                                    for (i = 0; i < parameters.size(); i++)
316                                            paramClasses[i] = parameters.get(i).getOpenType().getTypeName().toString();
317                                    String qName = name + Arrays.toString(paramClasses);
318                                    
319                                    operationsMap.put(qName, method);
320                            }
321                    }
322                    catch (Exception e) {
323                            throw new RuntimeException("Could not introspect MBean class: " + beanClass, e);
324                    }
325                    
326                    return new OpenMBeanInfoSupport(
327                            beanClass.getName(),
328                            description,
329                            attributes.toArray(new OpenMBeanAttributeInfo[attributes.size()]),
330                            new OpenMBeanConstructorInfo[0],
331                            operations.toArray(new OpenMBeanOperationInfo[operations.size()]),
332                            new MBeanNotificationInfo[0]
333                    );
334            }
335            
336            private static String getParameterName(Annotation[][] paramAnnotations, int index) {
337                    String name = null;
338                    if (paramAnnotations != null && paramAnnotations.length > index) {
339                            for (Annotation annot : paramAnnotations[index]) {
340                                    if (MBeanParameter.class.equals(annot.annotationType())) {
341                                            name = ((MBeanParameter)annot).name();
342                                            break;
343                                    }
344                            }
345                    }
346                    return (name != null ? name : "arg" + (index + 1));
347            }
348            
349            private static String getParameterDescription(Annotation[][] paramAnnotations, int index) {
350                    String description = null;
351                    if (paramAnnotations != null && paramAnnotations.length > index) {
352                            for (Annotation annot : paramAnnotations[index]) {
353                                    if (MBeanParameter.class.equals(annot.annotationType())) {
354                                            description = ((MBeanParameter)annot).description();
355                                            break;
356                                    }
357                            }
358                    }
359                    return (description != null ? description : "Operation Parameter " + (index + 1));
360            }
361            
362            private static OpenType getOpenType(Class<?> clazz) throws OpenDataException {
363                    
364                    if (SIMPLE_TYPES.containsKey(clazz))
365                            return SIMPLE_TYPES.get(clazz);
366                    
367                    if (clazz.isArray()) {
368                            int dimension = 1;
369                            Class<?> componentType = clazz.getComponentType();
370                            while (componentType.isArray()) {
371                                    dimension++;
372                                    componentType = componentType.getComponentType();
373                            }
374                            return new ArrayType(dimension, getOpenType(componentType));
375                    }
376                    
377                    throw new OpenDataException("Unsupported type: " + clazz);
378            }
379    
380            ///////////////////////////////////////////////////////////////////////////
381            // DynamicMBean implementation.
382    
383            /**
384             * {@inheritDoc}
385             */
386            public synchronized Object getAttribute(String attribute)
387                    throws AttributeNotFoundException, MBeanException, ReflectionException {
388                    
389                    PropertyDescriptor property = attributesMap.get(attribute);
390                    if (property != null && property.getReadMethod() != null) {
391                            try {
392                                    return property.getReadMethod().invoke(instance);
393                            }
394                            catch (Exception e) {
395                                    throw new ReflectionException(e, "Could not get attribute value: " + attribute);
396                            }
397                    }
398                    throw new AttributeNotFoundException("Attribute " + attribute + " not found");
399            }
400    
401            /**
402             * {@inheritDoc}
403             */
404            public synchronized AttributeList getAttributes(String[] names) {
405                    AttributeList attributes = new AttributeList();
406                    for (String name : names) {
407                            try {
408                                    attributes.add(new Attribute(name, getAttribute(name)));
409                            }
410                            catch (Exception e) {
411                                    // ignore...
412                            }
413                    }
414                    return attributes;
415            }
416    
417            /**
418             * {@inheritDoc}
419             */
420            public synchronized MBeanInfo getMBeanInfo() {
421                    return info;
422            }
423    
424            /**
425             * {@inheritDoc}
426             */
427            public synchronized Object invoke(String actionName, Object[] params, String[] signature)
428                    throws MBeanException, ReflectionException {
429                    
430                    if (signature == null)
431                            signature = new String[0];
432                    
433                    String qName = actionName + Arrays.toString(signature);
434                    
435                    MethodDescriptor method = operationsMap.get(qName);
436                    if (method == null)
437                            throw new RuntimeException("Method not found: " + qName);
438    
439                    try {
440                            return method.getMethod().invoke(instance, params);
441                    }
442                    catch (Exception e) {
443                            throw new ReflectionException(e, "Could not invoke operation: " + qName);
444                    }
445            }
446    
447            /**
448             * {@inheritDoc}
449             */
450            public synchronized void setAttribute(Attribute attribute)
451                    throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException {
452                    
453                    PropertyDescriptor property = attributesMap.get(attribute.getName());
454                    if (property != null && property.getWriteMethod() != null) {
455                            try {
456                                    property.getWriteMethod().invoke(instance, attribute.getValue());
457                            }
458                            catch (Exception e) {
459                                    throw new ReflectionException(e, "Could not set attribute value: " + attribute.getName() + "=" + attribute.getValue());
460                            }
461                    }
462                    else
463                            throw new AttributeNotFoundException("Attribute " + attribute.getName() + " not found");
464            }
465    
466            /**
467             * {@inheritDoc}
468             */
469            public synchronized AttributeList setAttributes(AttributeList attributes) {
470                    AttributeList returnedAttributes = new AttributeList();
471                    for (Object a : attributes) {
472                            Attribute attribute = (Attribute)a;
473                            try {
474                                    setAttribute(attribute);
475                                    returnedAttributes.add(new Attribute(attribute.getName(), getAttribute(attribute.getName())));
476                            }
477                            catch (Exception e) {
478                                    // Ignore...
479                            }
480                    }
481                    return returnedAttributes;
482            }
483    }