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