/*
 * @(#)JMXConnectorFactory.java	1.42 04/05/05
 * 
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package javax.management.remote;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.security.AccessController;
import java.security.PrivilegedAction;

import com.sun.jmx.remote.util.ClassLogger;
import com.sun.jmx.remote.util.EnvHelp;
import com.sun.jmx.remote.util.Service;

/**
 * <p>Factory to create JMX API connector clients.  There
 * are no instances of this class.</p>
 *
 * <p>Connections are usually made using the {@link
 * #connect(JMXServiceURL) connect} method of this class.  More
 * advanced applications can separate the creation of the connector
 * client, using {@link #newJMXConnector(JMXServiceURL, Map)
 * newJMXConnector} and the establishment of the connection itself, using
 * {@link JMXConnector#connect(Map)}.</p>
 *
 * <p>Each client is created by an instance of {@link
 * JMXConnectorProvider}.  This instance is found as follows.  Suppose
 * the given {@link JMXServiceURL} looks like
 * <code>"service:jmx:<em>protocol</em>:<em>remainder</em>"</code>.
 * Then the factory will attempt to find the appropriate {@link
 * JMXConnectorProvider} for <code><em>protocol</em></code>.  Each
 * occurrence of the character <code>+</code> or <code>-</code> in
 * <code><em>protocol</em></code> is replaced by <code>.</code> or
 * <code>_</code>, respectively.</p>
 *
 * <p>A <em>provider package list</em> is searched for as follows:</p>
 *
 * <ol>
 *
 * <li>If the <code>environment</code> parameter to {@link
 * #newJMXConnector(JMXServiceURL, Map) newJMXConnector} contains the
 * key <code>jmx.remote.protocol.provider.pkgs</code> then the
 * associated value is the provider package list.
 *
 * <li>Otherwise, if the system property
 * <code>jmx.remote.protocol.provider.pkgs</code> exists, then its value
 * is the provider package list.
 *
 * <li>Otherwise, there is no provider package list.
 *
 * </ol>
 *
 * <p>The provider package list is a string that is interpreted as a
 * list of non-empty Java package names separated by vertical bars
 * (<code>|</code>).  If the string is empty, then so is the provider
 * package list.  If the provider package list is not a String, or if
 * it contains an element that is an empty string, a {@link
 * JMXProviderException} is thrown.</p>
 *
 * <p>If the provider package list exists and is not empty, then for
 * each element <code><em>pkg</em></code> of the list, the factory
 * will attempt to load the class
 *
 * <blockquote>
 * <code><em>pkg</em>.<em>protocol</em>.ClientProvider</code>
 * </blockquote>

 * <p>If the <code>environment</code> parameter to {@link
 * #newJMXConnector(JMXServiceURL, Map) newJMXConnector} contains the
 * key <code>jmx.remote.protocol.provider.class.loader</code> then the
 * associated value is the class loader to use to load the provider.
 * If the associated value is not an instance of {@link
 * java.lang.ClassLoader}, an {@link
 * java.lang.IllegalArgumentException} is thrown.</p>
 *
 * <p>If the <code>jmx.remote.protocol.provider.class.loader</code>
 * key is not present in the <code>environment</code> parameter, the
 * calling thread's context class loader is used.</p>
 *
 * <p>If the attempt to load this class produces a {@link
 * ClassNotFoundException}, the search for a handler continues with
 * the next element of the list.</p>
 *
 * <p>Otherwise, a problem with the provider found is signalled by a
 * {@link JMXProviderException} whose {@link
 * JMXProviderException#getCause() <em>cause</em>} indicates the underlying
 * exception, as follows:</p>
 *
 * <ul>
 *
 * <li>if the attempt to load the class produces an exception other
 * than <code>ClassNotFoundException</code>, that is the
 * <em>cause</em>;
 *
 * <li>if {@link Class#newInstance()} for the class produces an
 * exception, that is the <em>cause</em>.
 *
 * </ul>
 *
 * <p>If no provider is found by the above steps, including the
 * default case where there is no provider package list, then the
 * implementation will use its own provider for
 * <code><em>protocol</em></code>, or it will throw a
 * <code>MalformedURLException</code> if there is none.  An
 * implementation may choose to find providers by other means.  For
 * example, it may support the <a
 * href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Service Provider">
 * JAR conventions for service providers</a>, where the service
 * interface is <code>JMXConnectorProvider</code>.</p>
 *
 * <p>Every implementation must support the RMI connector protocols,
 * specified with the string <code>rmi</code> or
 * <code>iiop</code>.</p>
 *
 * <p>Once a provider is found, the result of the
 * <code>newJMXConnector</code> method is the result of calling {@link
 * JMXConnectorProvider#newJMXConnector(JMXServiceURL,Map) newJMXConnector}
 * on the provider.</p>
 *
 * <p>The <code>Map</code> parameter passed to the
 * <code>JMXConnectorProvider</code> is a new read-only
 * <code>Map</code> that contains all the entries that were in the
 * <code>environment</code> parameter to {@link
 * #newJMXConnector(JMXServiceURL,Map)
 * JMXConnectorFactory.newJMXConnector}, if there was one.
 * Additionally, if the
 * <code>jmx.remote.protocol.provider.class.loader</code> key is not
 * present in the <code>environment</code> parameter, it is added to
 * the new read-only <code>Map</code>.  The associated value is the
 * calling thread's context class loader.</p>
 *
 * @since 1.5
 * @since.unbundled 1.0
 */
public class JMXConnectorFactory {

    /**
     * <p>Name of the attribute that specifies the default class
     * loader. This class loader is used to deserialize return values and
     * exceptions from remote <code>MBeanServerConnection</code>
     * calls.  The value associated with this attribute is an instance
     * of {@link ClassLoader}.</p>
     */
    public static final String DEFAULT_CLASS_LOADER =
        "jmx.remote.default.class.loader";

    /**
     * <p>Name of the attribute that specifies the provider packages
     * that are consulted when looking for the handler for a protocol.
     * The value associated with this attribute is a string with
     * package names separated by vertical bars (<code>|</code>).</p>
     */
    public static final String PROTOCOL_PROVIDER_PACKAGES =
        "jmx.remote.protocol.provider.pkgs";

    /**
     * <p>Name of the attribute that specifies the class
     * loader for loading protocol providers.
     * The value associated with this attribute is an instance
     * of {@link ClassLoader}.</p>
     */
    public static final String PROTOCOL_PROVIDER_CLASS_LOADER =
        "jmx.remote.protocol.provider.class.loader";

    private static final String PROTOCOL_PROVIDER_DEFAULT_PACKAGE =
	"com.sun.jmx.remote.protocol";

    private static final ClassLogger logger =
	new ClassLogger("javax.management.remote.misc", "JMXConnectorFactory");

    /** There are no instances of this class.  */
    private JMXConnectorFactory() {
    }

    /**
     * <p>Creates a connection to the connector server at the given
     * address.</p>
     *
     * <p>This method is equivalent to {@link
     * #connect(JMXServiceURL,Map) connect(serviceURL, null)}.</p>
     *
     * @param serviceURL the address of the connector server to
     * connect to.
     *
     * @return a <code>JMXConnector</code> whose {@link
     * JMXConnector#connect connect} method has been called.
     *
     * @exception NullPointerException if <code>serviceURL</code> is null.
     *
     * @exception IOException if the connector client or the
     * connection cannot be made because of a communication problem.
     *
     * @exception SecurityException if the connection cannot be made
     * for security reasons.
     */
    public static JMXConnector connect(JMXServiceURL serviceURL)
            throws IOException {
        return connect(serviceURL, null);
    }

    /**
     * <p>Creates a connection to the connector server at the given
     * address.</p>
     *
     * <p>This method is equivalent to:</p>
     *
     * <pre>
     * JMXConnector conn = JMXConnectorFactory.newJMXConnector(serviceURL,
     *                                                         environment);
     * conn.connect(environment);
     * </pre>
     *
     * @param serviceURL the address of the connector server to connect to.
     *
     * @param environment a set of attributes to determine how the
     * connection is made.  This parameter can be null.  Keys in this
     * map must be Strings.  The appropriate type of each associated
     * value depends on the attribute.  The contents of
     * <code>environment</code> are not changed by this call.
     *
     * @return a <code>JMXConnector</code> representing the newly-made
     * connection.  Each successful call to this method produces a
     * different object.
     *
     * @exception NullPointerException if <code>serviceURL</code> is null.
     *
     * @exception IOException if the connector client or the
     * connection cannot be made because of a communication problem.
     *
     * @exception SecurityException if the connection cannot be made
     * for security reasons.
     */
    public static JMXConnector connect(JMXServiceURL serviceURL,
                                       Map<String,?> environment)
            throws IOException {
	if (serviceURL == null)
	    throw new NullPointerException("Null JMXServiceURL");
        JMXConnector conn = newJMXConnector(serviceURL, environment);
        conn.connect(environment);
        return conn;
    }

    /**
     * <p>Creates a connector client for the connector server at the
     * given address.  The resultant client is not connected until its
     * {@link JMXConnector#connect(Map) connect} method is called.</p>
     *
     * @param serviceURL the address of the connector server to connect to.
     *
     * @param environment a set of attributes to determine how the
     * connection is made.  This parameter can be null.  Keys in this
     * map must be Strings.  The appropriate type of each associated
     * value depends on the attribute.  The contents of
     * <code>environment</code> are not changed by this call.
     *
     * @return a <code>JMXConnector</code> representing the new
     * connector client.  Each successful call to this method produces
     * a different object.
     *
     * @exception NullPointerException if <code>serviceURL</code> is null.
     *
     * @exception IOException if the connector client cannot be made
     * because of a communication problem.
     *
     * @exception MalformedURLException if there is no provider for the
     * protocol in <code>serviceURL</code>.
     *
     * @exception JMXProviderException if there is a provider for the
     * protocol in <code>serviceURL</code> but it cannot be used for
     * some reason.
     */
    public static JMXConnector newJMXConnector(JMXServiceURL serviceURL,
					       Map<String,?> environment)
            throws IOException {
        if (environment == null)
            environment = new HashMap();
        else {
	    EnvHelp.checkAttributes(environment);
            environment = new HashMap(environment);
	}
	
	final ClassLoader loader = resolveClassLoader(environment);
	final Class targetInterface = JMXConnectorProvider.class;
	final String protocol = serviceURL.getProtocol();
	final String providerClassName = "ClientProvider";
	
	JMXConnectorProvider provider =
	    (JMXConnectorProvider) getProvider(serviceURL, environment,
					       providerClassName,
					       targetInterface,
					       loader);
	if(provider == null) {
	    // Loader is null when context class loader is set to null
	    // and no loader has been provided in map.
	    // com.sun.jmx.remote.util.Service class extracted from j2se
	    // provider search algorithm doesn't handle well null classloader.
	    if(loader != null) {
		JMXConnector connection = getConnectorAsService(loader, 
								serviceURL,
								environment);
		if(connection != null)
		    return connection;
	    }
	    
	    provider = (JMXConnectorProvider)
		getProvider(protocol, PROTOCOL_PROVIDER_DEFAULT_PACKAGE,
			    JMXConnectorFactory.class.getClassLoader(),
			    providerClassName, targetInterface);
	}
	
	if(provider == null)
	    throw new MalformedURLException("Unsupported protocol: " + 
					    protocol);
	
        environment = Collections.unmodifiableMap(environment);
	
        return provider.newJMXConnector(serviceURL, environment);
    }

    private static String resolvePkgs(Map env) throws JMXProviderException {

        Object pkgsObject = null;

        if (env != null)
            pkgsObject = env.get(PROTOCOL_PROVIDER_PACKAGES);

        if (pkgsObject == null)
            pkgsObject =
		AccessController.doPrivileged(new PrivilegedAction() {
		    public Object run() {
			return System.getProperty(PROTOCOL_PROVIDER_PACKAGES);
		    }
		});

	if (pkgsObject == null)
	    return null;

	if (!(pkgsObject instanceof String)) {
	    final String msg = "Value of " + PROTOCOL_PROVIDER_PACKAGES +
		" parameter is not a String: " +
		pkgsObject.getClass().getName();
	    throw new JMXProviderException(msg);
	}

	final String pkgs = (String) pkgsObject;
	if (pkgs.trim().equals(""))
	    return null;

	// pkgs may not contain an empty element
	if (pkgs.startsWith("|") || pkgs.endsWith("|") ||
	    pkgs.indexOf("||") >= 0) {
	    final String msg = "Value of " + PROTOCOL_PROVIDER_PACKAGES +
		" contains an empty element: " + pkgs;
	    throw new JMXProviderException(msg);
	}

	return pkgs;
    }

    static Object getProvider(JMXServiceURL serviceURL,
			      Map environment, String providerClassName,
			      Class targetInterface,
			      ClassLoader loader)
            throws IOException {

        final String protocol = serviceURL.getProtocol();
	
        final String pkgs = resolvePkgs(environment);

	Object instance = null;
	
	if (pkgs != null) {
	    environment.put(PROTOCOL_PROVIDER_CLASS_LOADER, loader);
	    
	    instance =
		getProvider(protocol, pkgs, loader, providerClassName,
			    targetInterface);
	}

	return instance;
    }
    
    static Iterator getProviderIterator(final Class providerClass,
					final ClassLoader loader) {
	PrivilegedAction action = new PrivilegedAction() {
		public Object run() {
		    return Service.providers(providerClass, loader);
		}
	    };
	return (Iterator) AccessController.doPrivileged(action);
    }

    private static JMXConnector getConnectorAsService(ClassLoader loader, 
						      JMXServiceURL url, 
						      Map map) 
	throws IllegalArgumentException, JMXProviderException {

	Iterator providers = getProviderIterator(JMXConnectorProvider.class,
						 loader);
	JMXConnectorProvider provider = null;
	JMXConnector connection = null;
	while(providers.hasNext()) {
	    provider = 
		(JMXConnectorProvider) providers.next();
	    try {
		connection = provider.newJMXConnector(url, map);
		return connection;
	    } catch (JMXProviderException e) {
		throw e;
	    }		
	    catch (Exception e) {
		if (logger.traceOn())
		    logger.trace("getConnectorAsService",
				 "URL[" + url +
				 "] Service provider exception: " + e);
		continue;
	    }
	}
	return null;
    }
    
    static Object getProvider(String protocol,
			      String pkgs, 
			      ClassLoader loader,
			      String providerClassName,
			      Class targetInterface)
	    throws IOException {

        StringTokenizer tokenizer = new StringTokenizer(pkgs, "|");

        while (tokenizer.hasMoreTokens()) {
            String pkg = tokenizer.nextToken();
            String className = (pkg + "." + protocol2package(protocol) +
                                "." + providerClassName);
	    Class providerClass;
            try {
                providerClass = Class.forName(className, true, loader);
            } catch (ClassNotFoundException e) {
                //Add trace.
                continue;
            }

	    if (!targetInterface.isAssignableFrom(providerClass)) {
                final String msg =
                    "Provider class does not implement " +
                    targetInterface.getName() + ": " +
                    providerClass.getName();
                throw new JMXProviderException(msg);
            }

            try {
                return providerClass.newInstance();
            } catch (Exception e) {
                final String msg =
                    "Exception when instantiating provider [" + className +
                    "]";
                throw new JMXProviderException(msg, e);
            }
        }

	return null;
    }

    static ClassLoader resolveClassLoader(Map environment) {
        ClassLoader loader = null;

        if (environment != null) {
            try {
                loader = (ClassLoader)
                    environment.get(PROTOCOL_PROVIDER_CLASS_LOADER);
            } catch (ClassCastException e) {
                final String msg =
                    "The ClassLoader supplied in the environment map using " +
		    "the " + PROTOCOL_PROVIDER_CLASS_LOADER +
		    " attribute is not an instance of java.lang.ClassLoader";
                throw new IllegalArgumentException(msg); 
            }
        }

        if (loader == null)
	    loader = (ClassLoader)
		AccessController.doPrivileged(new PrivilegedAction() {
			public Object run() {
			    return
				Thread.currentThread().getContextClassLoader();
			}
		    });

        return loader;
    }
    
    private static String protocol2package(String protocol) {
        return protocol.replace('+', '.').replace('-', '_');
    }
}
