/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.manager;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serial;
import java.util.Set;

import javax.management.Attribute;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.OperationsException;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeData;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.mbeans.MBeanDumper;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;

/**
 * This servlet will dump JMX attributes in a simple format and implement proxy services for modeler.
 */
public class JMXProxyServlet extends HttpServlet {

    @Serial
    private static final long serialVersionUID = 1L;

    // Constant for "no parameters" when invoking a JMX operation
    // without any parameters.
    private static final String[] NO_PARAMETERS = new String[0];

    private static final StringManager sm = StringManager.getManager(JMXProxyServlet.class);

    // ----------------------------------------------------- Instance Variables
    /**
     * MBean server.
     */
    protected transient MBeanServer mBeanServer = null;
    protected transient Registry registry;


    // --------------------------------------------------------- Public Methods

    @Override
    public void init() throws ServletException {
        // Retrieve the MBean server
        registry = Registry.getRegistry(null);
        mBeanServer = Registry.getRegistry(null).getMBeanServer();
    }


    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.setContentType("text/plain;charset=" + Constants.CHARSET);
        // Stop older versions of IE thinking they know best. We set text/plain
        // in the line above for a reason. IE's behaviour is unwanted at best
        // and dangerous at worst.
        response.setHeader("X-Content-Type-Options", "nosniff");
        PrintWriter writer = response.getWriter();

        if (mBeanServer == null) {
            writer.println("Error - No mbean server");
            return;
        }

        String qry = request.getParameter("set");
        if (qry != null) {
            String name = request.getParameter("att");
            String val = request.getParameter("val");

            setAttribute(writer, qry, name, val);
            return;
        }
        qry = request.getParameter("get");
        if (qry != null) {
            String name = request.getParameter("att");
            getAttribute(writer, qry, name, request.getParameter("key"));
            return;
        }
        qry = request.getParameter("invoke");
        if (qry != null) {
            String opName = request.getParameter("op");
            String[] params = getInvokeParameters(request.getParameter("ps"));
            invokeOperation(writer, qry, opName, params);
            return;
        }
        qry = request.getParameter("qry");
        if (qry == null) {
            qry = "*:*";
        }

        listBeans(writer, qry);
    }


    public void getAttribute(PrintWriter writer, String onameStr, String att, String key) {
        try {
            ObjectName oname = new ObjectName(onameStr);
            Object value = mBeanServer.getAttribute(oname, att);

            if (null != key && value instanceof CompositeData) {
                value = ((CompositeData) value).get(key);
            }

            String valueStr;
            if (value != null) {
                valueStr = value.toString();
            } else {
                valueStr = "<null>";
            }

            writer.print("OK - Attribute get '");
            writer.print(onameStr);
            writer.print("' - ");
            writer.print(att);

            if (null != key) {
                writer.print(" - key '");
                writer.print(key);
                writer.print("'");
            }

            writer.print(" = ");

            writer.println(MBeanDumper.escape(valueStr));
        } catch (Exception e) {
            writer.println("Error - " + e.toString());
            e.printStackTrace(writer);
        }
    }


    public void setAttribute(PrintWriter writer, String onameStr, String att, String val) {
        try {
            setAttributeInternal(onameStr, att, val);
            writer.println("OK - Attribute set");
        } catch (Exception e) {
            writer.println("Error - " + e.toString());
            e.printStackTrace(writer);
        }
    }


    public void listBeans(PrintWriter writer, String qry) {

        Set<ObjectName> names;
        try {
            names = mBeanServer.queryNames(new ObjectName(qry), null);
            writer.println("OK - Number of results: " + names.size());
            writer.println();
        } catch (Exception e) {
            writer.println("Error - " + e.toString());
            e.printStackTrace(writer);
            return;
        }

        String dump = MBeanDumper.dumpBeans(mBeanServer, names);
        writer.print(dump);
    }


    /**
     * Determines if a type is supported by the {@link JMXProxyServlet}.
     *
     * @param type The type to check
     *
     * @return Always returns <code>true</code>
     */
    public boolean isSupported(String type) {
        return true;
    }


    private void invokeOperation(PrintWriter writer, String onameStr, String op, String[] valuesStr) {
        try {
            Object retVal = invokeOperationInternal(onameStr, op, valuesStr);
            if (retVal != null) {
                writer.println("OK - Operation " + op + " returned:");
                output("", writer, retVal);
            } else {
                writer.println("OK - Operation " + op + " without return value");
            }
        } catch (Exception e) {
            writer.println("Error - " + e.toString());
            e.printStackTrace(writer);
        }
    }


    /**
     * Parses parameter values from a parameter string.
     *
     * @param paramString The string containing comma-separated operation-invocation parameters, or <code>null</code> if
     *                        there are no parameters.
     *
     * @return An array of String parameters (empty array if <code>paramString</code> was <code>null</code>).
     */
    private String[] getInvokeParameters(String paramString) {
        if (paramString == null) {
            return NO_PARAMETERS;
        } else {
            return paramString.split(",");
        }
    }


    /**
     * Sets an MBean attribute's value.
     */
    private void setAttributeInternal(String onameStr, String attributeName, String value)
            throws OperationsException, MBeanException, ReflectionException {
        ObjectName oname = new ObjectName(onameStr);
        String type = registry.getType(oname, attributeName);
        Object valueObj = registry.convertValue(type, value);
        mBeanServer.setAttribute(oname, new Attribute(attributeName, valueObj));
    }


    /**
     * Invokes an operation on an MBean.
     *
     * @param onameStr   The name of the MBean.
     * @param operation  The name of the operation to invoke.
     * @param parameters An array of Strings containing the parameters to the operation. They will be converted to the
     *                       appropriate types to call the requested operation.
     *
     * @return The value returned by the requested operation.
     */
    @SuppressWarnings("null") // parameters can't be null if signature.length > 0
    private Object invokeOperationInternal(String onameStr, String operation, String[] parameters)
            throws OperationsException, MBeanException, ReflectionException {
        ObjectName oname = new ObjectName(onameStr);
        int paramCount = null == parameters ? 0 : parameters.length;
        MBeanOperationInfo methodInfo = registry.getMethodInfo(oname, operation, paramCount);
        if (null == methodInfo) {
            // getMethodInfo returns null for both "object not found" and "operation not found"
            MBeanInfo info;
            try {
                info = registry.getMBeanServer().getMBeanInfo(oname);
            } catch (InstanceNotFoundException infe) {
                throw infe;
            } catch (Exception e) {
                throw new IllegalArgumentException(sm.getString("jmxProxyServlet.noBeanFound", onameStr), e);
            }
            throw new IllegalArgumentException(sm.getString("jmxProxyServlet.noOperationOnBean", operation,
                    Integer.valueOf(paramCount), onameStr, info.getClassName()));
        }

        MBeanParameterInfo[] signature = methodInfo.getSignature();
        String[] signatureTypes = new String[signature.length];
        Object[] values = new Object[signature.length];
        for (int i = 0; i < signature.length; i++) {
            MBeanParameterInfo pi = signature[i];
            signatureTypes[i] = pi.getType();
            values[i] = registry.convertValue(pi.getType(), parameters[i]);
        }

        return mBeanServer.invoke(oname, operation, values, signatureTypes);
    }


    private void output(String indent, PrintWriter writer, Object result) {
        if (result instanceof Object[]) {
            for (Object obj : (Object[]) result) {
                output("  " + indent, writer, obj);
            }
        } else {
            String strValue;
            if (result != null) {
                strValue = result.toString();
            } else {
                strValue = "<null>";
            }
            writer.println(indent + strValue);
        }
    }
}
