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
021package org.granite.jmx;
022
023import java.beans.BeanInfo;
024import java.beans.Introspector;
025import java.beans.MethodDescriptor;
026import java.beans.PropertyDescriptor;
027import java.lang.annotation.Annotation;
028import java.lang.reflect.Method;
029import java.math.BigDecimal;
030import java.math.BigInteger;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.Date;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039
040import javax.management.Attribute;
041import javax.management.AttributeList;
042import javax.management.AttributeNotFoundException;
043import javax.management.DynamicMBean;
044import javax.management.InvalidAttributeValueException;
045import javax.management.MBeanException;
046import javax.management.MBeanInfo;
047import javax.management.MBeanNotificationInfo;
048import javax.management.MBeanOperationInfo;
049import javax.management.ObjectName;
050import javax.management.ReflectionException;
051import javax.management.openmbean.ArrayType;
052import javax.management.openmbean.OpenDataException;
053import javax.management.openmbean.OpenMBeanAttributeInfo;
054import javax.management.openmbean.OpenMBeanAttributeInfoSupport;
055import javax.management.openmbean.OpenMBeanConstructorInfo;
056import javax.management.openmbean.OpenMBeanInfoSupport;
057import javax.management.openmbean.OpenMBeanOperationInfo;
058import javax.management.openmbean.OpenMBeanOperationInfoSupport;
059import javax.management.openmbean.OpenMBeanParameterInfo;
060import javax.management.openmbean.OpenMBeanParameterInfoSupport;
061import javax.management.openmbean.OpenType;
062import 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" })
079public 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}