/*
 * 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.mbeans;

import java.util.Set;

import javax.management.DynamicMBean;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Group;
import org.apache.catalina.Role;
import org.apache.catalina.Server;
import org.apache.catalina.User;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.util.ContextName;
import org.apache.tomcat.util.descriptor.web.ContextEnvironment;
import org.apache.tomcat.util.descriptor.web.ContextResource;
import org.apache.tomcat.util.descriptor.web.ContextResourceLink;
import org.apache.tomcat.util.modeler.ManagedBean;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;

/**
 * Public utility methods in support of the server side MBeans implementation.
 */
public class MBeanUtils {

    /**
     * Default constructor.
     */
    public MBeanUtils() {
    }

    // ------------------------------------------------------- Static Variables

    /**
     * The string manager for this class.
     */
    protected static final StringManager sm = StringManager.getManager(MBeanUtils.class);

    /**
     * The set of exceptions to the normal rules used by <code>createManagedBean()</code>. The first element of each
     * pair is a class name, and the second element is the managed bean name.
     */
    private static final String[][] exceptions = { { "org.apache.catalina.users.MemoryGroup", "Group" },
            { "org.apache.catalina.users.MemoryRole", "Role" }, { "org.apache.catalina.users.MemoryUser", "User" },
            { "org.apache.catalina.users.GenericGroup", "Group" }, { "org.apache.catalina.users.GenericRole", "Role" },
            { "org.apache.catalina.users.GenericUser", "User" } };


    /**
     * The configuration information registry for our managed beans.
     */
    private static Registry registry = createRegistry();


    /**
     * The <code>MBeanServer</code> for this application.
     */
    private static MBeanServer mserver = createServer();


    // --------------------------------------------------------- Static Methods

    /**
     * Create and return the name of the <code>ManagedBean</code> that corresponds to this Catalina component.
     *
     * @param component The component for which to create a name
     */
    static String createManagedName(Object component) {

        // Deal with exceptions to the standard rule
        String className = component.getClass().getName();
        for (String[] exception : exceptions) {
            if (className.equals(exception[0])) {
                return exception[1];
            }
        }

        // Perform the standard transformation
        int period = className.lastIndexOf('.');
        if (period >= 0) {
            className = className.substring(period + 1);
        }
        return className;

    }


    /**
     * Create, register, and return an MBean for this <code>ContextEnvironment</code> object.
     *
     * @param environment The ContextEnvironment to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    public static DynamicMBean createMBean(ContextEnvironment environment) throws Exception {

        String mname = createManagedName(environment);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(environment);
        ObjectName oname = createObjectName(domain, environment);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>ContextResource</code> object.
     *
     * @param resource The ContextResource to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    public static DynamicMBean createMBean(ContextResource resource) throws Exception {

        String mname = createManagedName(resource);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(resource);
        ObjectName oname = createObjectName(domain, resource);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>ContextResourceLink</code> object.
     *
     * @param resourceLink The ContextResourceLink to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    public static DynamicMBean createMBean(ContextResourceLink resourceLink) throws Exception {

        String mname = createManagedName(resourceLink);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(resourceLink);
        ObjectName oname = createObjectName(domain, resourceLink);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>Group</code> object.
     *
     * @param group The Group to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    static DynamicMBean createMBean(Group group) throws Exception {

        String mname = createManagedName(group);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(group);
        ObjectName oname = createObjectName(domain, group);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>Role</code> object.
     *
     * @param role The Role to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    static DynamicMBean createMBean(Role role) throws Exception {

        String mname = createManagedName(role);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(role);
        ObjectName oname = createObjectName(domain, role);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>User</code> object.
     *
     * @param user The User to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    static DynamicMBean createMBean(User user) throws Exception {

        String mname = createManagedName(user);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(user);
        ObjectName oname = createObjectName(domain, user);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create, register, and return an MBean for this <code>UserDatabase</code> object.
     *
     * @param userDatabase The UserDatabase to be managed
     *
     * @return a new MBean
     *
     * @exception Exception if an MBean cannot be created or registered
     */
    static DynamicMBean createMBean(UserDatabase userDatabase) throws Exception {

        if (userDatabase.isSparse()) {
            // Register a sparse database bean as well
            ManagedBean managed = registry.findManagedBean("SparseUserDatabase");
            if (managed == null) {
                Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", "SparseUserDatabase"));
                throw new MBeanException(e);
            }
            String domain = managed.getDomain();
            if (domain == null) {
                domain = mserver.getDefaultDomain();
            }
            DynamicMBean mbean = managed.createMBean(userDatabase);
            ObjectName oname = createObjectName(domain, userDatabase);
            if (mserver.isRegistered(oname)) {
                mserver.unregisterMBean(oname);
            }
            mserver.registerMBean(mbean, oname);
        }

        String mname = createManagedName(userDatabase);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", mname));
            throw new MBeanException(e);
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        DynamicMBean mbean = managed.createMBean(userDatabase);
        ObjectName oname = createObjectName(domain, userDatabase);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }
        mserver.registerMBean(mbean, oname);
        return mbean;

    }


    /**
     * Create an <code>ObjectName</code> for this <code>Service</code> object.
     *
     * @param domain      Domain in which this name is to be created
     * @param environment The ContextEnvironment to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    public static ObjectName createObjectName(String domain, ContextEnvironment environment)
            throws MalformedObjectNameException {

        ObjectName name = null;
        String quotedEnvironmentName = ObjectName.quote(environment.getName());
        Object container = environment.getNamingResources().getContainer();
        if (container instanceof Server) {
            name = new ObjectName(domain + ":type=Environment" + ",resourcetype=Global,name=" + quotedEnvironmentName);
        } else if (container instanceof Context) {
            Context context = ((Context) container);
            ContextName cn = new ContextName(context.getName(), false);
            Container host = context.getParent();
            name = new ObjectName(domain + ":type=Environment" + ",resourcetype=Context,host=" + host.getName() +
                    ",context=" + cn.getDisplayName() + ",name=" + quotedEnvironmentName);
        }
        return name;

    }


    /**
     * Create an <code>ObjectName</code> for this <code>ContextResource</code> object.
     *
     * @param domain   Domain in which this name is to be created
     * @param resource The ContextResource to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    public static ObjectName createObjectName(String domain, ContextResource resource)
            throws MalformedObjectNameException {

        ObjectName name = null;
        String quotedResourceName = ObjectName.quote(resource.getName());
        Object container = resource.getNamingResources().getContainer();
        if (container instanceof Server) {
            name = new ObjectName(domain + ":type=Resource" + ",resourcetype=Global,class=" + resource.getType() +
                    ",name=" + quotedResourceName);
        } else if (container instanceof Context) {
            Context context = ((Context) container);
            ContextName cn = new ContextName(context.getName(), false);
            Container host = context.getParent();
            name = new ObjectName(domain + ":type=Resource" + ",resourcetype=Context,host=" + host.getName() +
                    ",context=" + cn.getDisplayName() + ",class=" + resource.getType() + ",name=" + quotedResourceName);
        }

        return name;

    }


    /**
     * Create an <code>ObjectName</code> for this <code>ContextResourceLink</code> object.
     *
     * @param domain       Domain in which this name is to be created
     * @param resourceLink The ContextResourceLink to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    public static ObjectName createObjectName(String domain, ContextResourceLink resourceLink)
            throws MalformedObjectNameException {

        ObjectName name = null;
        String quotedResourceLinkName = ObjectName.quote(resourceLink.getName());
        Object container = resourceLink.getNamingResources().getContainer();
        if (container instanceof Server) {
            name = new ObjectName(
                    domain + ":type=ResourceLink" + ",resourcetype=Global" + ",name=" + quotedResourceLinkName);
        } else if (container instanceof Context) {
            Context context = ((Context) container);
            ContextName cn = new ContextName(context.getName(), false);
            Container host = context.getParent();
            name = new ObjectName(domain + ":type=ResourceLink" + ",resourcetype=Context,host=" + host.getName() +
                    ",context=" + cn.getDisplayName() + ",name=" + quotedResourceLinkName);
        }

        return name;

    }


    /**
     * Create an <code>ObjectName</code> for this <code>Group</code> object.
     *
     * @param domain Domain in which this name is to be created
     * @param group  The Group to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    static ObjectName createObjectName(String domain, Group group) throws MalformedObjectNameException {

        return new ObjectName(domain + ":type=Group,groupname=" + ObjectName.quote(group.getGroupname()) +
                ",database=" + group.getUserDatabase().getId());

    }


    /**
     * Create an <code>ObjectName</code> for this <code>Role</code> object.
     *
     * @param domain Domain in which this name is to be created
     * @param role   The Role to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    static ObjectName createObjectName(String domain, Role role) throws MalformedObjectNameException {

        return new ObjectName(domain + ":type=Role,rolename=" + ObjectName.quote(role.getRolename()) + ",database=" +
                role.getUserDatabase().getId());

    }


    /**
     * Create an <code>ObjectName</code> for this <code>User</code> object.
     *
     * @param domain Domain in which this name is to be created
     * @param user   The User to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    static ObjectName createObjectName(String domain, User user) throws MalformedObjectNameException {

        return new ObjectName(domain + ":type=User,username=" + ObjectName.quote(user.getUsername()) + ",database=" +
                user.getUserDatabase().getId());
    }


    /**
     * Create an <code>ObjectName</code> for this <code>UserDatabase</code> object.
     *
     * @param domain       Domain in which this name is to be created
     * @param userDatabase The UserDatabase to be named
     *
     * @return a new object name
     *
     * @exception MalformedObjectNameException if a name cannot be created
     */
    static ObjectName createObjectName(String domain, UserDatabase userDatabase) throws MalformedObjectNameException {

        return new ObjectName(domain + ":type=UserDatabase,database=" + userDatabase.getId());

    }

    /**
     * Create and configure (if necessary) and return the registry of managed object descriptions.
     *
     * @return the singleton registry
     */
    public static synchronized Registry createRegistry() {
        if (registry == null) {
            registry = Registry.getRegistry(null);
            ClassLoader cl = MBeanUtils.class.getClassLoader();

            registry.loadDescriptors("org.apache.catalina.mbeans", cl);
            registry.loadDescriptors("org.apache.catalina.authenticator", cl);
            registry.loadDescriptors("org.apache.catalina.core", cl);
            registry.loadDescriptors("org.apache.catalina", cl);
            registry.loadDescriptors("org.apache.catalina.deploy", cl);
            registry.loadDescriptors("org.apache.catalina.loader", cl);
            registry.loadDescriptors("org.apache.catalina.realm", cl);
            registry.loadDescriptors("org.apache.catalina.session", cl);
            registry.loadDescriptors("org.apache.catalina.startup", cl);
            registry.loadDescriptors("org.apache.catalina.users", cl);
            registry.loadDescriptors("org.apache.catalina.ha", cl);
            registry.loadDescriptors("org.apache.catalina.connector", cl);
            registry.loadDescriptors("org.apache.catalina.valves", cl);
            registry.loadDescriptors("org.apache.catalina.storeconfig", cl);
            registry.loadDescriptors("org.apache.tomcat.util.descriptor.web", cl);
        }
        return registry;
    }


    /**
     * Create and configure (if necessary) and return the <code>MBeanServer</code> with which we will be registering our
     * <code>DynamicMBean</code> implementations.
     *
     * @return the singleton MBean server
     */
    public static synchronized MBeanServer createServer() {
        if (mserver == null) {
            mserver = Registry.getRegistry(null).getMBeanServer();
        }
        return mserver;
    }


    /**
     * Deregister the MBean for this <code>ContextEnvironment</code> object.
     *
     * @param environment The ContextEnvironment to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    public static void destroyMBean(ContextEnvironment environment) throws Exception {

        String mname = createManagedName(environment);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, environment);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }


    /**
     * Deregister the MBean for this <code>ContextResource</code> object.
     *
     * @param resource The ContextResource to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    public static void destroyMBean(ContextResource resource) throws Exception {

        // If this is a user database resource need to destroy groups, roles,
        // users and UserDatabase mbean
        if ("org.apache.catalina.UserDatabase".equals(resource.getType())) {
            destroyMBeanUserDatabase(resource.getName());
        }

        String mname = createManagedName(resource);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, resource);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }


    /**
     * Deregister the MBean for this <code>ContextResourceLink</code> object.
     *
     * @param resourceLink The ContextResourceLink to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    public static void destroyMBean(ContextResourceLink resourceLink) throws Exception {

        String mname = createManagedName(resourceLink);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, resourceLink);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }

    /**
     * Deregister the MBean for this <code>Group</code> object.
     *
     * @param group The Group to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    static void destroyMBean(Group group) throws Exception {

        String mname = createManagedName(group);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, group);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }


    /**
     * Deregister the MBean for this <code>Role</code> object.
     *
     * @param role The Role to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    static void destroyMBean(Role role) throws Exception {

        String mname = createManagedName(role);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, role);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }


    /**
     * Deregister the MBean for this <code>User</code> object.
     *
     * @param user The User to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    static void destroyMBean(User user) throws Exception {

        String mname = createManagedName(user);
        ManagedBean managed = registry.findManagedBean(mname);
        if (managed == null) {
            return;
        }
        String domain = managed.getDomain();
        if (domain == null) {
            domain = mserver.getDefaultDomain();
        }
        ObjectName oname = createObjectName(domain, user);
        if (mserver.isRegistered(oname)) {
            mserver.unregisterMBean(oname);
        }

    }


    /**
     * Deregister the MBean for the <code>UserDatabase</code> object with this name.
     *
     * @param userDatabase The UserDatabase to be managed
     *
     * @exception Exception if an MBean cannot be deregistered
     */
    static void destroyMBeanUserDatabase(String userDatabase) throws Exception {

        // FIXME: UserDatabase MBean management could be improved

        ObjectName query;
        Set<ObjectName> results;

        // Groups
        query = new ObjectName("Users:type=Group,database=" + userDatabase + ",*");
        results = mserver.queryNames(query, null);
        for (ObjectName result : results) {
            mserver.unregisterMBean(result);
        }

        // Roles
        query = new ObjectName("Users:type=Role,database=" + userDatabase + ",*");
        results = mserver.queryNames(query, null);
        for (ObjectName result : results) {
            mserver.unregisterMBean(result);
        }

        // Users
        query = new ObjectName("Users:type=User,database=" + userDatabase + ",*");
        results = mserver.queryNames(query, null);
        for (ObjectName result : results) {
            mserver.unregisterMBean(result);
        }

        // The database itself
        ObjectName db = new ObjectName("Users:type=UserDatabase,database=" + userDatabase);
        if (mserver.isRegistered(db)) {
            mserver.unregisterMBean(db);
        }
        db = new ObjectName("Catalina:type=UserDatabase,database=" + userDatabase);
        if (mserver.isRegistered(db)) {
            mserver.unregisterMBean(db);
        }
    }
}
