/*
 * Copyright (C) The MX4J Contributors.
 * All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0.
 * See the terms of the MX4J License in the documentation provided with this software.
 */

package mx4j.remote;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.StringTokenizer;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXConnectorProvider;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXConnectorServerProvider;
import javax.management.remote.JMXProviderException;
import javax.management.remote.JMXServiceURL;

import mx4j.log.Logger;

/**
 * @version $Revision: 1.4 $
 */
public class ProviderFactory extends ProviderHelper
{
   public static JMXConnectorProvider newJMXConnectorProvider(JMXServiceURL url, Map env) throws IOException
   {
      // Yes, throw NPE if url is null (spec compliant)
      String protocol = normalizeProtocol(url.getProtocol());
      String providerPackages = findProviderPackageList(env, JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES);
      ClassLoader classLoader = findProviderClassLoader(env, JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER);
      JMXConnectorProvider provider = (JMXConnectorProvider)loadProvider(providerPackages, protocol, MX4JRemoteConstants.CLIENT_PROVIDER_CLASS, classLoader);
      return provider;
   }

   public static JMXConnectorServerProvider newJMXConnectorServerProvider(JMXServiceURL url, Map env) throws IOException
   {
      // Yes, throw NPE if url is null (spec compliant)
      String protocol = normalizeProtocol(url.getProtocol());
      String providerPackages = findProviderPackageList(env, JMXConnectorServerFactory.PROTOCOL_PROVIDER_PACKAGES);
      ClassLoader classLoader = findProviderClassLoader(env, JMXConnectorServerFactory.PROTOCOL_PROVIDER_CLASS_LOADER);
      JMXConnectorServerProvider provider = (JMXConnectorServerProvider)loadProvider(providerPackages, protocol, MX4JRemoteConstants.SERVER_PROVIDER_CLASS, classLoader);
      return provider;
   }

   private static String findEnvironmentProviderPackageList(Map environment, String key) throws JMXProviderException
   {
      String providerPackages = null;
      if (environment != null)
      {
         Logger logger = getLogger();
         Object pkgs = environment.get(key);
         if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider packages in the environment: " + pkgs);
         if (pkgs != null && !(pkgs instanceof String)) throw new JMXProviderException("Provider package list must be a string");
         providerPackages = (String)pkgs;
      }
      return providerPackages;
   }

   private static String findProviderPackageList(Map environment, final String providerPkgsKey) throws JMXProviderException
   {
      // 1. Look in the environment
      // 2. Look for system property
      // 3. Use implementation's provider

      String providerPackages = findEnvironmentProviderPackageList(environment, providerPkgsKey);

      if (providerPackages == null)
      {
         providerPackages = findSystemPackageList(providerPkgsKey);
      }

      if (providerPackages != null && providerPackages.trim().length() == 0) throw new JMXProviderException("Provider package list cannot be an empty string");

      if (providerPackages == null)
         providerPackages = MX4JRemoteConstants.PROVIDER_PACKAGES;
      else
         providerPackages += MX4JRemoteConstants.PROVIDER_PACKAGES_SEPARATOR + MX4JRemoteConstants.PROVIDER_PACKAGES;

      Logger logger = getLogger();
      if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider packages list is: " + providerPackages);

      return providerPackages;
   }

   private static ClassLoader findProviderClassLoader(Map environment, String providerLoaderKey)
   {
      Logger logger = getLogger();

      ClassLoader classLoader = null;
      if (environment != null)
      {
         Object loader = environment.get(providerLoaderKey);
         if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider classloader in the environment: " + loader);
         if (loader != null && !(loader instanceof ClassLoader)) throw new IllegalArgumentException("Provider classloader is not a ClassLoader");
         classLoader = (ClassLoader)loader;
      }

      if (classLoader == null)
      {
         classLoader = Thread.currentThread().getContextClassLoader();
         if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider classloader in the environment: " + classLoader);
      }

      // Add the classloader as required by the spec
      environment.put(JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER, classLoader);
      if (logger.isEnabledFor(Logger.TRACE)) logger.trace("Provider classloader added to the environment");

      return classLoader;
   }

   private static Object loadProvider(String packages, String protocol, String className, ClassLoader loader) throws JMXProviderException, MalformedURLException
   {
      Logger logger = getLogger();

      StringTokenizer tokenizer = new StringTokenizer(packages, MX4JRemoteConstants.PROVIDER_PACKAGES_SEPARATOR);
      while (tokenizer.hasMoreTokens())
      {
         String pkg = tokenizer.nextToken().trim();
         if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider package: " + pkg);

         // The spec states the package cannot be empty
         if (pkg.length() == 0) throw new JMXProviderException("Empty package list not allowed: " + packages);

         String providerClassName = constructClassName(pkg, protocol, className);

         Class providerClass = null;
         try
         {
            providerClass = loadClass(providerClassName, loader);
         }
         catch (ClassNotFoundException x)
         {
            if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Provider class " + providerClassName + " not found, continuing with next package");
            continue;
         }
         catch (Exception x)
         {
            if (logger.isEnabledFor(Logger.TRACE)) logger.trace("Cannot load provider class " + providerClassName, x);
            throw new JMXProviderException("Cannot load provider class " + providerClassName, x);
         }

         try
         {
            return providerClass.newInstance();
         }
         catch (Exception x)
         {
            if (logger.isEnabledFor(Logger.TRACE)) logger.trace("Cannot instantiate provider class " + providerClassName, x);
            throw new JMXProviderException("Cannot instantiate provider class " + providerClassName, x);
         }
      }

      // Nothing found
      if (logger.isEnabledFor(Logger.DEBUG)) logger.debug("Could not find provider for protocol " + protocol + " in package list '" + packages + "'");
      throw new MalformedURLException("Could not find provider for protocol " + protocol + " in package list '" + packages + "'");
   }
}
