/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008-2009 Bull S.A.S
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.management.extensions.base;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;

import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeErrorException;
import javax.management.RuntimeMBeanException;
import javax.management.RuntimeOperationsException;
import javax.management.remote.JMXConnector;

import org.ow2.jonas.lib.management.extensions.util.ConnectorUtils;
import org.ow2.jonas.management.extensions.base.api.ManagementException;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Wrapper class allowing to apply a management operations (getAttribute,
 * setAttribute, invoke, etc.) on the corresponding to managed server's
 * connection. The managed server is identified by a name (serverName
 * parameter). The corresponding connection is kept in the serverConnections
 * table.
 * @author Adriana Danes
 * @author S. Ali Tokmen
 */
public class JonasManagementRepr {

    /**
     * Logger for traces.
     */
    protected static Log logger = LogFactory.getLog(JonasManagementRepr.class);

    /**
     * Cache of managed MBeanServer connection's provided by the DomainMonitor.
     */
    private static Hashtable<String, MBeanServerConnection> serverConnections = new Hashtable<String, MBeanServerConnection>();

    /**
     * Cache of JMXConnectors created on behalf of management client
     * applications.
     */
    private static List<ConnectorItem> connectorsList = Collections.synchronizedList(new ArrayList<ConnectorItem>());

    /**
     * Private constructor for utility class
     */
    private JonasManagementRepr() {
    }

    /**
     * Return the MBeanServer connection corresponding to a given server in the
     * current domain. The connection for servers in the domain are provided by
     * the DomainMonitor.
     * @param serverName The managed server name
     * @return The MBeanServerConnection corresponding to the managed server
     * @throws ManagementException Couldn't get the connection
     */
    public static MBeanServerConnection getServerConnection(final String serverName) throws ManagementException {
        MBeanServerConnection connection = serverConnections.get(serverName);
        if (connection == null) {
            if (LocalManagementContext.getInstance().getServerName().equals(serverName)) {
                // this is the local server (got a local connection to the MBean
                // Server in the current JVM)
                connection = LocalManagementContext.getInstance().getMbeanServer();
            } else {
                // this is a remote server, get the JMX connector address
                String jmxUrl = null;
                try {
                    jmxUrl = AdminJmxHelper.getConnectionUrl(serverName, LocalManagementContext.getInstance().getDomainName(),
                            LocalManagementContext.getInstance().getServerName());
                } catch (MalformedObjectNameException me) {
                    throw new ManagementException("Can't get JMX connector address for server " + serverName + me.toString());
                }
                // TODO support JMX authentification
                String username = null;
                String password = null;
                connection = getServerConnection(jmxUrl, username, password);
            }
            // keep the connection in the cache, then return it
            if (connection != null) {
                serverConnections.put(serverName, connection);
                return connection;
            } else {
                throw new ManagementException("Cant't get connection for server " + serverName);
            }
        } else {
            // return cached connection
            return connection;
        }
    }

    /**
     * Return the MBeanServer connection corresponding to a given server in the
     * current domain. The connection for servers in the domain are provided by
     * the DomainManager.
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return The MBeanServerConnection corresponding to the managed server
     * @throws ManagementException Couldn't get the connection
     */
    public static MBeanServerConnection getServerConnection(final String jmxUrl, final String username, final String password)
            throws ManagementException {
        JMXConnector connector = null;
        ConnectorItem item = null;
        int index = positionItem(jmxUrl, username, password);
        if (index >= 0 && connectorsList.get(index).getConnector() != null) {
            // found JMXConnector in the ConnecorItem list
            item = connectorsList.get(index);
            connector = item.getConnector();
        } else {
            // construct a JMXConnector
            try {
                connector = ConnectorUtils.getConnector(jmxUrl, username, password);
            } catch (Exception e) {
                throw new ManagementException("Can't manage server having jmx url " + jmxUrl.toString()
                        + " as couldn't get connector" + e.getCause());
            }
            if (index < 0) {
                // insert item in head of list
                item = addConnectorItem(jmxUrl, username, password);
            } else {
                // get item
                item = connectorsList.get(index);
            }
            if (item != null) {
                // update item
                item.setConnector(connector);
            }
        }
        MBeanServerConnection connection = null;
        try {
            // For a given JMXConnector, two successful calls to this method
            // will usually return the same MBeanServerConnection
            // object, though this is not required !!
            connection = connector.getMBeanServerConnection();
        } catch (IOException ioe) {
            // a valid MBeanServerConnection cannot be created, for instance
            // because the connection to the remote MBean server
            // has not yet been established (with the connect method), or it has
            // been closed, or it has broken
            try {
                connector.close();
                item.setConnector(null);
            } catch (IOException ioec) {
                throw new ManagementException("Can't manage server having jmx url " + jmxUrl.toString()
                        + " as couldn't get connection beacuse of IOException, and can't close connector");
            }
            throw new ManagementException("Can't manage server having jmx url " + jmxUrl.toString()
                    + " as couldn't get connection beacuse of IOException");
        }
        return connection;
    }

    /**
     * Implementation of the <code>isRegistered</code> method to be applied to a
     * server identified by a JMX Url.
     * @return True if the MBean is already registered in the server's MBean
     *         server, false otherwise or if an exception is catched.
     * @param on ObjectName of the MBean we are looking for
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @throws ManagementException management operation failed
     */
    public static boolean isRegistered(final ObjectName on, final String jmxUrl, final String username, final String password)
            throws ManagementException {
        MBeanServerConnection repr = null;
        boolean result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            result = repr.isRegistered(on);
        } catch (IOException e) {
            throw new ManagementException("Error calling isRegistered", e);
        }
        return result;
    }

    /**
     * Implementation of the <code>isRegistered</code> method to be applied to a
     * server in the domain.
     * @return True if the MBean is already registered in the MBean server,
     *         false otherwise or if an exception is catched.
     * @param on ObjectName of the MBean we are looking for
     * @param serverName The server name
     * @throws ManagementException management operation failed
     */
    public static boolean isRegistered(final ObjectName on, final String serverName) throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return repr.isRegistered(on);
        } catch (IOException e) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error calling isRegistered on server " + serverName, e);
        }
    }

    /**
     * Gets the value of a specific attribute of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param attribute A String specifying the name of the attribute to be
     *        retrieved.
     * @param serverName The server name
     * @return The value of the attribute.
     * @throws ManagementException management operation failed
     */
    public static Object getAttribute(final ObjectName on, final String attribute, final String serverName)
            throws ManagementException {
        /**
         * Get the server connection from the <code>serverConnections</code>
         * table.
         */
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return repr.getAttribute(on, attribute);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error getting attribute: " + attribute + 
                                          " on server " + serverName + " objectName=" + on, ioe);
        } catch (Exception e) {
            throw new ManagementException("Error getting attribute : " + attribute + 
                                          " on server " + serverName + " objectName=" + on, e);
        }
    }

    /**
     * Gets the value of a specific attribute of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param attribute A String specifying the name of the attribute to be
     *        retrieved.
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return The value of the attribute.
     * @throws ManagementException management operation failed
     */
    public static Object getAttribute(final ObjectName on, final String attribute, final String jmxUrl, final String username,
            final String password) throws ManagementException {
        MBeanServerConnection repr = null;
        Object result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            result = repr.getAttribute(on, attribute);
        } catch (Exception e) {
            throw new ManagementException("Error calling getting attribute: ", e);
        }
        return result;
    }

    /**
     * Gets the values of several attributes of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param attributes Array of attribute names to be retrieved.
     * @param serverName The server name
     * @return The value of the attribute.
     * @throws ManagementException management operation failed
     */
    public static AttributeList getAttributes(final ObjectName on, final String[] attributes, final String serverName)
            throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return repr.getAttributes(on, attributes);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error getting attributes, first attribute is " + attributes[0], ioe);
        } catch (Exception e) {
            throw new ManagementException("Error getting attributes, first attribute is " + attributes[0], e);
        }
    }

    /**
     * Gets the values of several attributes of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param attributes Array of attribute names to be retrieved.
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return The value of the attribute.
     * @throws ManagementException management operation failed
     */
    public static Object getAttributes(final ObjectName on, final String[] attributes, final String jmxUrl,
            final String username, final String password) throws ManagementException {
        MBeanServerConnection repr = null;
        Object result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            result = repr.getAttributes(on, attributes);
        } catch (Exception e) {
            throw new ManagementException("Error calling getting attribute: ", e);
        }
        return result;
    }

    /**
     * Sets the value of a specific attribute of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param serverName The server name
     * @param attribute A String specifying the name of the attribute to be set.
     * @param value The value to set to the attribute.
     * @throws ManagementException management operation failed
     */
    public static void setAttribute(final ObjectName on, final String attribute, final Object value, final String serverName)
            throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            repr.setAttribute(on, new Attribute(attribute, value));
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error setting attribute: " + attribute, ioe);
        } catch (Exception e) {
            throw new ManagementException("Error setting attribute: " + attribute, e);
        }
    }

    /**
     * Sets the value of a specific attribute of a named MBean.
     * @param on The ObjectName of the MBean.
     * @param attribute A String specifying the name of the attribute to be set.
     * @param value The value to set to the attribute.
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @throws ManagementException management operation failed
     */
    public static void setAttribute(final ObjectName on, final String attribute, final Object value, final String jmxUrl,
            final String username, final String password) throws ManagementException {
        MBeanServerConnection repr = null;
        Object result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            repr.setAttribute(on, new Attribute(attribute, value));
        } catch (Exception e) {
            throw new ManagementException("Error calling getting attribute: ", e);
        }
    }

    /**
     * @param on The ObjectName of the MBean within which the attribute is to be
     *        set.
     * @param serverName The server name
     * @param attributes A list of attributes: The identification of the
     *        attribute to be set and the value it is to be set to
     * @throws ManagementException management operation failed
     */
    public static void setAttributes(final ObjectName on, final AttributeList attributes, final String serverName)
            throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            repr.setAttributes(on, attributes);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error setting attributes, first attribute is " + attributes.get(0), ioe);
        } catch (Exception e) {
            throw new ManagementException("Error setting attributes, first attribute is " + attributes.get(0), e);
        }
    }

    /**
     * @param on The ObjectName of the MBean within which the attribute is to be
     *        set.
     * @param attributes A list of attributes: The identification of the
     *        attribute to be set and the value it is to be set to
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @throws ManagementException management operation failed
     */
    public static void setAttributes(final ObjectName on, final AttributeList attributes, final String jmxUrl,
            final String username, final String password) throws ManagementException {
        MBeanServerConnection repr = null;
        Object result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            repr.setAttributes(on, attributes);
        } catch (Exception e) {
            throw new ManagementException("Error calling getting attribute: ", e);
        }
    }

    /**
     * Implementation of the <code>invoke</code> method to be applied to a
     * server in the domain.
     * @param on the ObjectName of the MBean that is the target of the invoke.
     * @param operation operation to invoke
     * @param param invoke parameters
     * @param signature invoke parameters signature
     * @param serverName The server's name
     * @return The object returned by the operation
     * @throws ManagementException management operation failed
     */
    public static Object invoke(final ObjectName on, final String operation, final Object[] param, final String[] signature,
            final String serverName) throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return invoke(repr, on, operation, param, signature);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error while invoking opeartion: ", ioe);
        } catch (ManagementException e) {
            throw e;
        }
    }

    /**
     * Implementation of the <code>invoke</code> method to be applied to a
     * server in the domain.
     * @param on the ObjectName of the MBean that is the target of the invoke.
     * @param operation operation to invoke
     * @param param invoke parameters
     * @param signature invoke parameters signature
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return The object returned by the operation
     * @throws ManagementException management operation failed
     */
    public static Object invoke(final ObjectName on, final String operation, final Object[] param, final String[] signature,
            final String jmxUrl, final String username, final String password) throws ManagementException {
        MBeanServerConnection repr = null;
        Object result;
        try {
            repr = getServerConnection(jmxUrl, username, password);
        } catch (ManagementException e) {
            throw e;
        }
        try {
            return invoke(repr, on, operation, param, signature);
        } catch (Exception ioe) {
            throw new ManagementException("Error while invoking opeartion: ", ioe);
        }
    }

    private static Object invoke(final MBeanServerConnection repr, final ObjectName on, final String operation,
            final Object[] param, final String[] signature) throws IOException, ManagementException {
        Object result = null;
        try {
            result = repr.invoke(on, operation, param, signature);
        } catch (IOException ioe) {
            throw ioe;
        } catch (Exception e) {
            String message = e.getMessage();
            message = message + " - " + e.getCause().getMessage();
            Throwable exc = null;
            if (e instanceof MBeanException || e instanceof ReflectionException || e instanceof RuntimeMBeanException
                    || e instanceof RuntimeOperationsException || e instanceof RuntimeErrorException) {

                Exception targetExc = null;
                if (e instanceof MBeanException) {
                    targetExc = ((MBeanException) e).getTargetException();
                } else if (e instanceof ReflectionException) {
                    targetExc = ((ReflectionException) e).getTargetException();
                } else if (e instanceof RuntimeMBeanException) {
                    targetExc = ((RuntimeMBeanException) e).getTargetException();
                } else if (e instanceof RuntimeOperationsException) {
                    targetExc = ((RuntimeOperationsException) e).getTargetException();
                } else if (e instanceof RuntimeErrorException) {
                    Error atargetExc = ((RuntimeErrorException) e).getTargetError();
                    targetExc = new Exception(atargetExc.getMessage());
                }
                exc = targetExc;
            } else {
                exc = e;
            }
            throw new ManagementException(message, exc);
        }
        return result;
    }

    /**
     * @return A set containing the ObjectNames for the MBeans selected.
     * @param on MBean name
     * @param serverName The server name
     * @throws ManagementException management operation failed
     */
    public static Set queryNames(final ObjectName on, final String serverName) throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return repr.queryNames(on, null);
        } catch (IOException e) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error while getting MBean names on server " + serverName, e);
        }
    }

    /**
     * @return An instance of MBeanInfo allowing the retrieval of all attributes
     *         and operations of this MBean.
     * @param name MBean's ObjectName
     * @param serverName The server name
     * @throws ManagementException management operation failed
     */
    public static MBeanInfo getMBeanInfo(final ObjectName on, final String serverName) throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            return repr.getMBeanInfo(on);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error while getting MBean info on server " + serverName, ioe);
        } catch (Exception e) {
            throw new ManagementException("Error while getting MBean info on server " + serverName, e);
        }
    }

    /**
     * Unregisters an MBean from the managed server's MBean server.
     * @param on The object name of the MBean.
     * @param serverName The server name
     * @throws ManagementException wraps exception thrown by the called
     *         management operation
     */
    public static void unregisterMBean(final ObjectName on, final String serverName) throws ManagementException {
        MBeanServerConnection repr = getServerConnection(serverName);
        try {
            repr.unregisterMBean(on);
        } catch (IOException ioe) {
            // Connection has become obsolete.
            serverConnections.remove(serverName);
            throw new ManagementException("Error while unregistering MBean on server " + serverName, ioe);
        } catch (Exception e) {
            throw new ManagementException("Error while unregistering MBean on server " + serverName, e);
        }
    }

    /**
     * Create a ConnectorItem and add it in the head of the ConnectorItems list.
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return the created ConnectorItem
     */
    private static ConnectorItem addConnectorItem(final String jmxUrl, final String username, final String password) {
        ConnectorItem item = new ConnectorItem();
        item.setJmxUrlString(jmxUrl);
        item.setPassword(password);
        item.setPassword(password);
        connectorsList.add(0, item);
        return item;
    }

    /**
     * Determine the position of a ConnectorItem in the list, if any, having the
     * given parameters
     * @param jmxUrl a server's jmx url.
     * @param username the user name in case the jmx server is protected by
     *        user/password.
     * @param password the password in case the jmx server is protected by
     *        user/password.
     * @return the ConnectorItem's position or -1 if no item having the given
     *         parameters was found
     */
    private static int positionItem(final String jmxUrl, final String username, final String password) {
        for (int i = 0; i < connectorsList.size(); i++) {
            ConnectorItem cit = connectorsList.get(i);
            if (cit.equals(jmxUrl, username, password)) {
                return i;
            }
        }
        return -1;
    }
}
