/*
 * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

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.WeakHashMap;

import javax.management.Descriptor;
import javax.management.ImmutableDescriptor;
import javax.management.InvalidAttributeValueException;
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.NotCompliantMBeanException;
import javax.management.NotificationBroadcaster;
import javax.management.ReflectionException;
import sun.reflect.misc.ReflectUtil;

/**
 * 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.
 */
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);
    /**
     * 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 the methods to be analyzed to build the MBean interface.
     */
    final List<Method> getMethods(final Class<?> mbeanType) {
        ReflectUtil.checkPackageAccess(mbeanType);
        return Arrays.asList(mbeanType.getMethods());
    }

    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<>(mbeanInterface, this, analyzer,
                            mbeanInfo);
                    wr = new WeakReference<>(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) {
        final MBeanInfoMaker maker = new MBeanInfoMaker();
        analyzer.visit(maker);
        final String description =
                "Information on the management interface of the MBean";
        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> {

        public void visitAttribute(String attributeName,
                M getter,
                M setter) {
            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) {
        MBeanInfo mbi =
                getClassMBeanInfo(resource.getClass(), perInterface);
        MBeanNotificationInfo[] notifs = findNotifications(resource);
        if (notifs == null || notifs.length == 0)
            return mbi;
        else {
            return new MBeanInfo(mbi.getClassName(),
                    mbi.getDescription(),
                    mbi.getAttributes(),
                    mbi.getConstructors(),
                    mbi.getOperations(),
                    notifs,
                    mbi.getDescriptor());
        }
    }

    /**
     * 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<>();
                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;
        }
    }

    static MBeanNotificationInfo[] findNotifications(Object moi) {
        if (!(moi instanceof NotificationBroadcaster))
            return null;
        MBeanNotificationInfo[] mbn =
                ((NotificationBroadcaster) moi).getNotificationInfo();
        if (mbn == null)
            return null;
        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;
    }

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

}
