jdk/src/share/classes/com/sun/jmx/mbeanserver/MBeanIntrospector.java
author emcmanus
Tue, 09 Dec 2008 12:01:07 +0100
changeset 1697 98a530cd0594
parent 1636 eb801ce73ac9
child 1699 3611e5fd6da5
permissions -rw-r--r--
6774918: @NotificationInfo is ineffective on MBeans that cannot send notifications Reviewed-by: jfdenise

/*
 * Copyright 2005-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package com.sun.jmx.mbeanserver;


import static com.sun.jmx.mbeanserver.Util.*;

import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import javax.management.Description;

import javax.management.Descriptor;
import javax.management.ImmutableDescriptor;
import javax.management.IntrospectionException;
import javax.management.InvalidAttributeValueException;
import javax.management.MBean;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MXBean;
import javax.management.ManagedAttribute;
import javax.management.ManagedOperation;
import javax.management.NotCompliantMBeanException;
import javax.management.NotificationBroadcaster;
import javax.management.NotificationInfo;
import javax.management.NotificationInfos;
import javax.management.ReflectionException;

/**
 * An introspector for MBeans of a certain type.  There is one instance
 * of this class for Standard MBeans, and one for every MXBeanMappingFactory;
 * these two cases correspond to the two concrete subclasses of this abstract
 * class.
 *
 * @param <M> the representation of methods for this kind of MBean:
 * Method for Standard MBeans, ConvertingMethod for MXBeans.
 *
 * @since 1.6
 */
/*
 * Using a type parameter <M> allows us to deal with the fact that
 * Method and ConvertingMethod have no useful common ancestor, on
 * which we could call getName, getGenericReturnType, etc.  A simpler approach
 * would be to wrap every Method in an object that does have a common
 * ancestor with ConvertingMethod.  But that would mean an extra object
 * for every Method in every Standard MBean interface.
 */
public abstract class MBeanIntrospector<M> {
    static final class PerInterfaceMap<M>
            extends WeakHashMap<Class<?>, WeakReference<PerInterface<M>>> {}

    /** The map from interface to PerInterface for this type of MBean. */
    abstract PerInterfaceMap<M> getPerInterfaceMap();
    /**
     * The map from concrete implementation class and interface to
     * MBeanInfo for this type of MBean.
     */
    abstract MBeanInfoMap getMBeanInfoMap();

    /** Make an interface analyzer for this type of MBean. */
    abstract MBeanAnalyzer<M> getAnalyzer(Class<?> mbeanInterface)
    throws NotCompliantMBeanException;

    /** True if MBeans with this kind of introspector are MXBeans. */
    abstract boolean isMXBean();

    /** Find the M corresponding to the given Method. */
    abstract M mFrom(Method m);

    /** Get the name of this method. */
    abstract String getName(M m);

    /**
     * Get the return type of this method.  This is the return type
     * of a method in a Java interface, so for MXBeans it is the
     * declared Java type, not the mapped Open Type.
     */
    abstract Type getGenericReturnType(M m);

    /**
     * Get the parameter types of this method in the Java interface
     * it came from.
     */
    abstract Type[] getGenericParameterTypes(M m);

    /**
     * Get the signature of this method as a caller would have to supply
     * it in MBeanServer.invoke.  For MXBeans, the named types will be
     * the mapped Open Types for the parameters.
     */
    abstract String[] getSignature(M m);

    /**
     * Check that this method is valid.  For example, a method in an
     * MXBean interface is not valid if one of its parameters cannot be
     * mapped to an Open Type.
     */
    abstract void checkMethod(M m);

    /**
     * Invoke the method with the given target and arguments.
     *
     * @param cookie Additional information about the target.  For an
     * MXBean, this is the MXBeanLookup associated with the MXBean.
     */
    /*
     * It would be cleaner if the type of the cookie were a
     * type parameter to this class, but that would involve a lot of
     * messy type parameter propagation just to avoid a couple of casts.
     */
    abstract Object invokeM2(M m, Object target, Object[] args, Object cookie)
    throws InvocationTargetException, IllegalAccessException,
            MBeanException;

    /**
     * Test whether the given value is valid for the given parameter of this
     * M.
     */
    abstract boolean validParameter(M m, Object value, int paramNo,
            Object cookie);

    /**
     * Construct an MBeanAttributeInfo for the given attribute based on the
     * given getter and setter.  One but not both of the getter and setter
     * may be null.
     */
    abstract MBeanAttributeInfo getMBeanAttributeInfo(String attributeName,
            M getter, M setter) throws IntrospectionException;

    final String getAttributeDescription(
            String attributeName, String defaultDescription,
            Method getter, Method setter) throws IntrospectionException {
        String g = Introspector.descriptionForElement(getter);
        String s = Introspector.descriptionForElement(setter);
        if (g == null) {
            if (s == null)
                return defaultDescription;
            else
                return s;
        } else if (s == null || g.equals(s)) {
            return g;
        } else {
            throw new IntrospectionException(
                    "Inconsistent @Description on getter and setter for " +
                    "attribute " + attributeName);
        }
    }

    /**
     * Construct an MBeanOperationInfo for the given operation based on
     * the M it was derived from.
     */
    abstract MBeanOperationInfo getMBeanOperationInfo(String operationName,
            M operation);

    /**
     * Get a Descriptor containing fields that MBeans of this kind will
     * always have.  For example, MXBeans will always have "mxbean=true".
     */
    abstract Descriptor getBasicMBeanDescriptor();

    /**
     * Get a Descriptor containing additional fields beyond the ones
     * from getBasicMBeanDescriptor that MBeans whose concrete class
     * is resourceClass will always have.
     */
    abstract Descriptor getMBeanDescriptor(Class<?> resourceClass);

    /**
     * Get any additional Descriptor entries for this introspector instance.
     * If there is a non-default MXBeanMappingFactory, it will appear in
     * this Descriptor.
     * @return Additional Descriptor entries, or an empty Descriptor if none.
     */
    Descriptor getSpecificMBeanDescriptor() {
        return ImmutableDescriptor.EMPTY_DESCRIPTOR;
    }

    void checkCompliance(Class<?> mbeanType) throws NotCompliantMBeanException {
        if (!mbeanType.isInterface() &&
                !mbeanType.isAnnotationPresent(MBean.class) &&
                !Introspector.hasMXBeanAnnotation(mbeanType)) {
            throw new NotCompliantMBeanException("Not an interface and " +
                    "does not have @" + MBean.class.getSimpleName() +
                    " or @" + MXBean.class.getSimpleName() + " annotation: " +
                    mbeanType.getName());
        }
    }

    /**
     * Get the methods to be analyzed to build the MBean interface.
     */
    List<Method> getMethods(final Class<?> mbeanType) throws Exception {
        if (mbeanType.isInterface())
            return Arrays.asList(mbeanType.getMethods());

        final List<Method> methods = newList();
        getAnnotatedMethods(mbeanType, methods);
        return methods;
    }

    final PerInterface<M> getPerInterface(Class<?> mbeanInterface)
    throws NotCompliantMBeanException {
        PerInterfaceMap<M> map = getPerInterfaceMap();
        synchronized (map) {
            WeakReference<PerInterface<M>> wr = map.get(mbeanInterface);
            PerInterface<M> pi = (wr == null) ? null : wr.get();
            if (pi == null) {
                try {
                    MBeanAnalyzer<M> analyzer = getAnalyzer(mbeanInterface);
                    MBeanInfo mbeanInfo =
                            makeInterfaceMBeanInfo(mbeanInterface, analyzer);
                    pi = new PerInterface<M>(mbeanInterface, this, analyzer,
                            mbeanInfo);
                    wr = new WeakReference<PerInterface<M>>(pi);
                    map.put(mbeanInterface, wr);
                } catch (Exception x) {
                    throw Introspector.throwException(mbeanInterface,x);
                }
            }
            return pi;
        }
    }

    /**
     * Make the MBeanInfo skeleton for the given MBean interface using
     * the given analyzer.  This will never be the MBeanInfo of any real
     * MBean (because the getClassName() must be a concrete class), but
     * its MBeanAttributeInfo[] and MBeanOperationInfo[] can be inserted
     * into such an MBeanInfo, and its Descriptor can be the basis for
     * the MBeanInfo's Descriptor.
     */
    private MBeanInfo makeInterfaceMBeanInfo(Class<?> mbeanInterface,
            MBeanAnalyzer<M> analyzer) throws IntrospectionException {
        final MBeanInfoMaker maker = new MBeanInfoMaker();
        analyzer.visit(maker);
        final String defaultDescription =
                "Information on the management interface of the MBean";
        String description = Introspector.descriptionForElement(mbeanInterface);
        if (description == null)
            description = defaultDescription;
        return maker.makeMBeanInfo(mbeanInterface, description);
    }

    /** True if the given getter and setter are consistent. */
    final boolean consistent(M getter, M setter) {
        return (getter == null || setter == null ||
                getGenericReturnType(getter).equals(getGenericParameterTypes(setter)[0]));
    }

    /**
     * Invoke the given M on the given target with the given args and cookie.
     * Wrap exceptions appropriately.
     */
    final Object invokeM(M m, Object target, Object[] args, Object cookie)
    throws MBeanException, ReflectionException {
        try {
            return invokeM2(m, target, args, cookie);
        } catch (InvocationTargetException e) {
            unwrapInvocationTargetException(e);
            throw new RuntimeException(e); // not reached
        } catch (IllegalAccessException e) {
            throw new ReflectionException(e, e.toString());
        }
        /* We do not catch and wrap RuntimeException or Error,
         * because we're in a DynamicMBean, so the logic for DynamicMBeans
         * will do the wrapping.
         */
    }

    /**
     * Invoke the given setter on the given target with the given argument
     * and cookie.  Wrap exceptions appropriately.
     */
    /* If the value is of the wrong type for the method we are about to
     * invoke, we are supposed to throw an InvalidAttributeValueException.
     * Rather than making the check always, we invoke the method, then
     * if it throws an exception we check the type to see if that was
     * what caused the exception.  The assumption is that an exception
     * from an invalid type will arise before any user method is ever
     * called (either in reflection or in OpenConverter).
     */
    final void invokeSetter(String name, M setter, Object target, Object arg,
            Object cookie)
            throws MBeanException, ReflectionException,
            InvalidAttributeValueException {
        try {
            invokeM2(setter, target, new Object[] {arg}, cookie);
        } catch (IllegalAccessException e) {
            throw new ReflectionException(e, e.toString());
        } catch (RuntimeException e) {
            maybeInvalidParameter(name, setter, arg, cookie);
            throw e;
        } catch (InvocationTargetException e) {
            maybeInvalidParameter(name, setter, arg, cookie);
            unwrapInvocationTargetException(e);
        }
    }

    private void maybeInvalidParameter(String name, M setter, Object arg,
            Object cookie)
            throws InvalidAttributeValueException {
        if (!validParameter(setter, arg, 0, cookie)) {
            final String msg =
                    "Invalid value for attribute " + name + ": " + arg;
            throw new InvalidAttributeValueException(msg);
        }
    }

    static boolean isValidParameter(Method m, Object value, int paramNo) {
        Class<?> c = m.getParameterTypes()[paramNo];
        try {
            // Following is expensive but we only call this method to determine
            // if an exception is due to an incompatible parameter type.
            // Plain old c.isInstance doesn't work for primitive types.
            Object a = Array.newInstance(c, 1);
            Array.set(a, 0, value);
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    private static void
            unwrapInvocationTargetException(InvocationTargetException e)
            throws MBeanException {
        Throwable t = e.getCause();
        if (t instanceof RuntimeException)
            throw (RuntimeException) t;
        else if (t instanceof Error)
            throw (Error) t;
        else
            throw new MBeanException((Exception) t,
                    (t == null ? null : t.toString()));
    }

    /** A visitor that constructs the per-interface MBeanInfo. */
    private class MBeanInfoMaker
            implements MBeanAnalyzer.MBeanVisitor<M, IntrospectionException> {

        public void visitAttribute(String attributeName,
                M getter,
                M setter) throws IntrospectionException {
            MBeanAttributeInfo mbai =
                    getMBeanAttributeInfo(attributeName, getter, setter);

            attrs.add(mbai);
        }

        public void visitOperation(String operationName,
                M operation) {
            MBeanOperationInfo mboi =
                    getMBeanOperationInfo(operationName, operation);

            ops.add(mboi);
        }

        /** Make an MBeanInfo based on the attributes and operations
         *  found in the interface. */
        MBeanInfo makeMBeanInfo(Class<?> mbeanInterface,
                String description) {
            final MBeanAttributeInfo[] attrArray =
                    attrs.toArray(new MBeanAttributeInfo[0]);
            final MBeanOperationInfo[] opArray =
                    ops.toArray(new MBeanOperationInfo[0]);
            final String interfaceClassName =
                    "interfaceClassName=" + mbeanInterface.getName();
            final Descriptor classNameDescriptor =
                    new ImmutableDescriptor(interfaceClassName);
            final Descriptor mbeanDescriptor = getBasicMBeanDescriptor();
            final Descriptor annotatedDescriptor =
                    Introspector.descriptorForElement(mbeanInterface);
            final Descriptor descriptor =
                DescriptorCache.getInstance().union(
                    classNameDescriptor,
                    mbeanDescriptor,
                    annotatedDescriptor);

            return new MBeanInfo(mbeanInterface.getName(),
                    description,
                    attrArray,
                    null,
                    opArray,
                    null,
                    descriptor);
        }

        private final List<MBeanAttributeInfo> attrs = newList();
        private final List<MBeanOperationInfo> ops = newList();
    }

    /*
     * Looking up the MBeanInfo for a given base class (implementation class)
     * is complicated by the fact that we may use the same base class with
     * several different explicit MBean interfaces via the
     * javax.management.StandardMBean class.  It is further complicated
     * by the fact that we have to be careful not to retain a strong reference
     * to any Class object for fear we would prevent a ClassLoader from being
     * garbage-collected.  So we have a first lookup from the base class
     * to a map for each interface that base class might specify giving
     * the MBeanInfo constructed for that base class and interface.
     */
    static class MBeanInfoMap
            extends WeakHashMap<Class<?>, WeakHashMap<Class<?>, MBeanInfo>> {
    }

    /**
     * Return the MBeanInfo for the given resource, based on the given
     * per-interface data.
     */
    final MBeanInfo getMBeanInfo(Object resource, PerInterface<M> perInterface)
    throws NotCompliantMBeanException {
        MBeanInfo mbi =
                getClassMBeanInfo(resource.getClass(), perInterface);
        MBeanNotificationInfo[] notifs;
        try {
            notifs = findNotifications(resource);
        } catch (RuntimeException e) {
            NotCompliantMBeanException x =
                    new NotCompliantMBeanException(e.getMessage());
            x.initCause(e);
            throw x;
        }
        Descriptor d = getSpecificMBeanDescriptor();
        boolean anyNotifs = (notifs != null && notifs.length > 0);
        if (!anyNotifs && ImmutableDescriptor.EMPTY_DESCRIPTOR.equals(d))
            return mbi;
        else {
            d = ImmutableDescriptor.union(d, mbi.getDescriptor());
            return new MBeanInfo(mbi.getClassName(),
                    mbi.getDescription(),
                    mbi.getAttributes(),
                    mbi.getConstructors(),
                    mbi.getOperations(),
                    notifs,
                    d);
        }
    }

    /**
     * Return the basic MBeanInfo for resources of the given class and
     * per-interface data.  This MBeanInfo might not be the final MBeanInfo
     * for instances of the class, because if the class is a
     * NotificationBroadcaster then each instance gets to decide what
     * MBeanNotificationInfo[] to put in its own MBeanInfo.
     */
    final MBeanInfo getClassMBeanInfo(Class<?> resourceClass,
            PerInterface<M> perInterface) {
        MBeanInfoMap map = getMBeanInfoMap();
        synchronized (map) {
            WeakHashMap<Class<?>, MBeanInfo> intfMap = map.get(resourceClass);
            if (intfMap == null) {
                intfMap = new WeakHashMap<Class<?>, MBeanInfo>();
                map.put(resourceClass, intfMap);
            }
            Class<?> intfClass = perInterface.getMBeanInterface();
            MBeanInfo mbi = intfMap.get(intfClass);
            if (mbi == null) {
                MBeanInfo imbi = perInterface.getMBeanInfo();
                Descriptor descriptor =
                        ImmutableDescriptor.union(imbi.getDescriptor(),
                        getMBeanDescriptor(resourceClass));
                mbi = new MBeanInfo(resourceClass.getName(),
                        imbi.getDescription(),
                        imbi.getAttributes(),
                        findConstructors(resourceClass),
                        imbi.getOperations(),
                        (MBeanNotificationInfo[]) null,
                        descriptor);
                intfMap.put(intfClass, mbi);
            }
            return mbi;
        }
    }

    /*
     * Add to "methods" every public method that has the @ManagedAttribute
     * or @ManagedOperation annotation, in the given class or any of
     * its superclasses or superinterfaces.
     *
     * We always add superclass or superinterface methods first, so that
     * the stable sort used by eliminateCovariantMethods will put the
     * method from the most-derived class last.  This means that we will
     * see the version of the @ManagedAttribute (or ...Operation) annotation
     * from that method, which might have a different description or whatever.
     */
    private static void getAnnotatedMethods(Class<?> c, List<Method> methods)
    throws Exception {
        Class<?> sup = c.getSuperclass();
        if (sup != null)
            getAnnotatedMethods(sup, methods);
        Class<?>[] intfs = c.getInterfaces();
        for (Class<?> intf : intfs)
            getAnnotatedMethods(intf, methods);
        for (Method m : c.getMethods()) {
            // We are careful not to add m if it is inherited from a parent
            // class or interface, because duplicate methods lead to nasty
            // behaviour in eliminateCovariantMethods.
            if (m.getDeclaringClass() == c &&
                    (m.isAnnotationPresent(ManagedAttribute.class) ||
                     m.isAnnotationPresent(ManagedOperation.class)))
                methods.add(m);
        }
    }

    /*
     * Return the array of MBeanNotificationInfo for the given MBean object.
     * If the object implements NotificationBroadcaster and its
     * getNotificationInfo() method returns a non-empty array, then that
     * is the result.  Otherwise, if the object has a @NotificationInfo
     * or @NotificationInfos annotation, then its contents form the result.
     * Otherwise, the result is null.
     */
    static MBeanNotificationInfo[] findNotifications(Object moi) {
        if (moi instanceof NotificationBroadcaster) {
            MBeanNotificationInfo[] mbn =
                    ((NotificationBroadcaster) moi).getNotificationInfo();
            if (mbn != null && mbn.length > 0) {
                MBeanNotificationInfo[] result =
                        new MBeanNotificationInfo[mbn.length];
                for (int i = 0; i < mbn.length; i++) {
                    MBeanNotificationInfo ni = mbn[i];
                    if (ni.getClass() != MBeanNotificationInfo.class)
                        ni = (MBeanNotificationInfo) ni.clone();
                    result[i] = ni;
                }
                return result;
            }
        } else {
            try {
                if (!MBeanInjector.injectsSendNotification(moi))
                    return null;
            } catch (NotCompliantMBeanException e) {
                throw new RuntimeException(e);
            }
        }
        return findNotificationsFromAnnotations(moi.getClass());
    }

    public static MBeanNotificationInfo[] findNotificationsFromAnnotations(
            Class<?> mbeanClass) {
        Class<?> c = getAnnotatedNotificationInfoClass(mbeanClass);
        if (c == null)
            return null;
        NotificationInfo ni = c.getAnnotation(NotificationInfo.class);
        NotificationInfos nis = c.getAnnotation(NotificationInfos.class);
        List<NotificationInfo> list = newList();
        if (ni != null)
            list.add(ni);
        if (nis != null)
            list.addAll(Arrays.asList(nis.value()));
        if (list.isEmpty())
            return null;
        List<MBeanNotificationInfo> mbnis = newList();
        for (NotificationInfo x : list) {
            // The Descriptor includes any fields explicitly specified by
            // x.descriptorFields(), plus any fields from the contained
            // @Description annotation.
            Descriptor d = new ImmutableDescriptor(x.descriptorFields());
            d = ImmutableDescriptor.union(
                    d, Introspector.descriptorForAnnotation(x.description()));
            MBeanNotificationInfo mbni = new MBeanNotificationInfo(
                    x.types(), x.notificationClass().getName(),
                    x.description().value(), d);
            mbnis.add(mbni);
        }
        return mbnis.toArray(new MBeanNotificationInfo[mbnis.size()]);
    }

    private static final Map<Class<?>, WeakReference<Class<?>>>
            annotatedNotificationInfoClasses = newWeakHashMap();

    private static Class<?> getAnnotatedNotificationInfoClass(Class<?> baseClass) {
        synchronized (annotatedNotificationInfoClasses) {
            WeakReference<Class<?>> wr =
                    annotatedNotificationInfoClasses.get(baseClass);
            if (wr != null)
                return wr.get();
            Class<?> c = null;
            if (baseClass.isAnnotationPresent(NotificationInfo.class) ||
                    baseClass.isAnnotationPresent(NotificationInfos.class)) {
                c = baseClass;
            } else {
                Class<?>[] intfs = baseClass.getInterfaces();
                for (Class<?> intf : intfs) {
                    Class<?> c1 = getAnnotatedNotificationInfoClass(intf);
                    if (c1 != null) {
                        if (c != null) {
                            throw new IllegalArgumentException(
                                    "Class " + baseClass.getName() + " inherits " +
                                    "@NotificationInfo(s) from both " +
                                    c.getName() + " and " + c1.getName());
                        }
                        c = c1;
                    }
                }
            }
            // Record the result of the search.  If no @NotificationInfo(s)
            // were found, c is null, and we store a WeakReference(null).
            // This prevents us from having to search again and fail again.
            annotatedNotificationInfoClasses.put(baseClass,
                    new WeakReference<Class<?>>(c));
            return c;
        }
    }

    private static MBeanConstructorInfo[] findConstructors(Class<?> c) {
        Constructor<?>[] cons = c.getConstructors();
        MBeanConstructorInfo[] mbc = new MBeanConstructorInfo[cons.length];
        for (int i = 0; i < cons.length; i++) {
            String descr = "Public constructor of the MBean";
            Description d = cons[i].getAnnotation(Description.class);
            if (d != null)
                descr = d.value();
            mbc[i] = new MBeanConstructorInfo(descr, cons[i]);
        }
        return mbc;
    }

}