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