/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.deployment.spi;

import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.enterprise.deploy.shared.ModuleType;
import javax.enterprise.deploy.spi.TargetModuleID;
import javax.enterprise.deploy.spi.exceptions.TargetException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.jboss.logging.Logger;

/**
 * A Target that deploys using the JMX adaptor to communicate with the
 * MainDeployer using file URLs to the deployments. This target is selected
 * by including a targetType=jmx param in the DeploymentManager deployURI.
 * 
 * @author Thomas.Diesler@jboss.org
 * @author Scott.Stark@jboss.com
 * @version $Revision: 81011 $
 */
public class JMXTarget implements JBossTarget
{
   private static final Logger log = Logger.getLogger(JMXTarget.class);
   public static final String DESCRIPTION = "JBoss JMX deployment target";
   /**
    * The default RMIAdaptor JNDI location
    */
   private static final String DEFAULT_ADAPTOR_PATH = "/jmx/invoker/RMIAdaptor";
   /** The JSR88 deployment manager default and uri option name */
   private static final String JSR88_MBEAN = "jboss.management.local:type=JSR88DeploymentManager,name=DefaultManager";
   private static final String JSR88_MBEAN_OPT = "jsr88MBean";

   /** The deployment target uri */
   private URI deployURI;
   /** The JNDI properties used to locate the MBeanServer */
   private Properties jndiEnv;
   private ObjectName jsr88MBean;

   /**
    * @todo merge the query parameter parsing for jndi and mbean names and
    * test proper escaping of unsafe url chars.
    * 
    * @param deployURI
    */
   public JMXTarget(URI deployURI)
   {
      log.debug("new JMXTarget: " + deployURI);
      try
      {
         String localHostName = InetAddress.getLocalHost().getHostName();

         String scheme = deployURI.getScheme();
         String host = deployURI.getHost();
         int port = deployURI.getPort();
         String path = deployURI.getPath();
         String query = deployURI.getRawQuery();

         String uri = deployURI.toASCIIString();
         if (uri.startsWith(DeploymentManagerImpl.DEPLOYER_URI))
         {
            // Using JNDI defaults
            scheme = "jnp";
            host = localHostName;
            port = 1099;
            path = DEFAULT_ADAPTOR_PATH;

            try
            {
               InitialContext iniCtx = new InitialContext();
               Hashtable env = iniCtx.getEnvironment();
               String providerURL = (String)env.get(InitialContext.PROVIDER_URL);
               if (providerURL != null)
               {
                  // In case there is no schema returned
                  if (providerURL.indexOf("://") < 0)
                     providerURL = "jnp://" + providerURL;

                  URI providerURI = new URI(providerURL);
                  scheme = providerURI.getScheme();
                  host = providerURI.getHost();
                  port = providerURI.getPort();
               }
            }
            catch (NamingException e)
            {
               log.error(e);
            }
         }

         StringBuffer tmp = new StringBuffer(scheme + "://");
         tmp.append(host != null ? host : localHostName);
         tmp.append(port > 0 ? ":" + port : "");
         tmp.append(path != null && path.length() > 0 ? path : DEFAULT_ADAPTOR_PATH);
         tmp.append(query != null ? "?" + query : "");
         deployURI = new URI(tmp.toString());

         log.debug("URI changed to: " + deployURI);
         this.deployURI = deployURI;

         // Parse the query for options
         parseQuery();
      }
      catch (Exception e)
      {
         log.error(e);
      }

   }

   /**
    * Get the target's description
    * @return the description
    */
   public String getDescription()
   {
      return DESCRIPTION;
   }

   /**
    * Get the target's name
    * @return the name
    */
   public String getName()
   {
      return deployURI.toString();
   }

   /**
    * Get the target's host name
    */
   public String getHostName()
   {
      return deployURI.getHost();
   }

   /**
    * Deploy a given module
    */
   public void deploy(TargetModuleID targetModuleID) throws Exception
   {
      TargetModuleIDImpl moduleID = (TargetModuleIDImpl)targetModuleID;
      SerializableTargetModuleID smoduleID = new SerializableTargetModuleID(moduleID);
      MBeanServerConnection server = getMBeanServerConnection();
      String url = targetModuleID.getModuleID();
      Object[] args = { smoduleID };
      String[] sig = { smoduleID.getClass().getName() };
      log.info("Begin deploy: " + url);
      server.invoke(jsr88MBean, "deploy", args, sig);
      log.info("End deploy");
   }

   /**
    * Start a given module
    */
   public void start(TargetModuleID targetModuleID) throws Exception
   {
      MBeanServerConnection server = getMBeanServerConnection();
      URL url = new URL(targetModuleID.getModuleID());
      Object[] args = { url };
      String[] sig = { URL.class.getName() };
      log.debug("Start: " + url);
      args = new Object[] { url.toExternalForm() };
      sig = new String[] { String.class.getName() };
      log.info("Begin start: " + url);
      server.invoke(jsr88MBean, "start", args, sig);
      log.info("End start");
   }

   /**
    * Stop a given module
    */
   public void stop(TargetModuleID targetModuleID) throws Exception
   {
      MBeanServerConnection server = getMBeanServerConnection();
      URL url = new URL(targetModuleID.getModuleID());
      Object[] args = { url };
      String[] sig = { URL.class.getName() };
      log.debug("Stop: " + url);
      args = new Object[] { url.toExternalForm() };
      sig = new String[] { String.class.getName() };
      log.info("Begin stop: " + url);
      server.invoke(jsr88MBean, "stop", args, sig);
      log.info("End stop");
   }

   /**
    * Undeploy a given module
    */
   public void undeploy(TargetModuleID targetModuleID) throws Exception
   {
      MBeanServerConnection server = getMBeanServerConnection();
      String url = targetModuleID.getModuleID();
      Object[] args = { url };
      String[] sig = { String.class.getName() };
      log.info("Begin undeploy: " + url);
      server.invoke(jsr88MBean, "undeploy", args, sig);
      log.info("End undeploy");
   }

   /**
    * Retrieve the list of all J2EE application modules running or not running
    * on the identified targets.
    */
   public TargetModuleID[] getAvailableModules(ModuleType moduleType) throws TargetException
   {
      try
      {
         List list = new ArrayList();
         MBeanServerConnection server = getMBeanServerConnection();
         Object[] args = { new Integer(moduleType.getValue()) };
         String[] sig = { int.class.getName() };
         SerializableTargetModuleID[] modules = (SerializableTargetModuleID[])server.invoke(jsr88MBean, "getAvailableModules", args, sig);
         for (int n = 0; n < modules.length; n++)
         {
            SerializableTargetModuleID id = modules[n];
            String moduleID = id.getModuleID();
            boolean isRunning = id.isRunning();
            ModuleType type = ModuleType.getModuleType(id.getModuleType());
            TargetModuleIDImpl tmid = new TargetModuleIDImpl(this, moduleID, null, isRunning, type);
            convertChildren(tmid, id);
            list.add(tmid);
         }

         TargetModuleID[] targetModuleIDs = new TargetModuleID[list.size()];
         list.toArray(targetModuleIDs);
         return targetModuleIDs;
      }
      catch (Exception e)
      {
         TargetException tex = new TargetException("Failed to get available modules");
         tex.initCause(e);
         throw tex;
      }
   }

   private void convertChildren(TargetModuleIDImpl parent, SerializableTargetModuleID parentID)
   {
      SerializableTargetModuleID[] children = parentID.getChildModuleIDs();
      int length = children != null ? children.length : 0;
      for (int n = 0; n < length; n++)
      {
         SerializableTargetModuleID id = children[n];
         String moduleID = id.getModuleID();
         boolean isRunning = id.isRunning();
         ModuleType type = ModuleType.getModuleType(id.getModuleType());
         TargetModuleIDImpl child = new TargetModuleIDImpl(this, moduleID, parent, isRunning, type);
         parent.addChildTargetModuleID(child);
         convertChildren(child, id);
      }
   }

   // Get MBeanServerConnection
   private MBeanServerConnection getMBeanServerConnection() throws NamingException
   {
      Properties env = buildJNDIEnv();
      String lookupPath = deployURI.getPath();
      log.debug("JNDI lookup: " + lookupPath);
      log.trace("Creating InitialContext with env: " + env);
      InitialContext ctx = new InitialContext(env);
      MBeanServerConnection server = (MBeanServerConnection)ctx.lookup(lookupPath);
      return server;
   }

   /**
    * Parse the query portion of the deployment URI to look for options:
    * jsr88MBean : specifies the JSR88 mbean service that provides the
    *    deployment manager deploy/start/stop/undeploy operations. 
    */
   private void parseQuery() throws Exception
   {
      String query = deployURI.getRawQuery();
      log.debug("DeployURI.rawQuery: " + query);
      Properties params = new Properties();
      if (query != null)
      {
         /* Break the raw query into the name=value pairs. This processing is
          too fragile to how the URI was built as it expects that only
          the name/value portions of the query were encoded.
          */
         String[] options = query.split("[&=]");
         for (int n = 0; n < options.length; n += 2)
         {
            String name = URLDecoder.decode(options[n], "UTF-8");
            String value = URLDecoder.decode(options[n + 1], "UTF-8");
            params.setProperty(name, value);
         }
      }

      String name = params.getProperty(JSR88_MBEAN_OPT, JSR88_MBEAN);
      jsr88MBean = new ObjectName(name);
   }

   private Properties buildJNDIEnv()
   {
      if (jndiEnv == null)
      {
         jndiEnv = new Properties();

         // Parse the query string for name=value pairs to put into the env
         String query = deployURI.getQuery();
         if (query != null)
         {
            log.debug("Parsing query string: " + query);
            StringTokenizer tokenizer = new StringTokenizer(query, "=&");
            while (tokenizer.hasMoreTokens())
            {
               String name = tokenizer.nextToken();
               String value = tokenizer.nextToken();
               jndiEnv.setProperty(name, value);
            }
         }

         // Set defaults for missing properties
         if (jndiEnv.getProperty(Context.INITIAL_CONTEXT_FACTORY) == null)
         {
            jndiEnv.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
         }
         if (jndiEnv.getProperty(Context.PROVIDER_URL) == null)
         {
            String host = deployURI.getHost();
            if (host == null)
            {
               try
               {
                  host = InetAddress.getLocalHost().getHostName();
               }
               catch (UnknownHostException e)
               {
                  host = "localhost";
               }
            }

            int port = deployURI.getPort();
            if (port <= 0)
            {
               port = 1099;
            }

            String jnpURL = "jnp://" + host + ':' + port;
            jndiEnv.setProperty(Context.PROVIDER_URL, jnpURL);
         }
         if (jndiEnv.getProperty(Context.OBJECT_FACTORIES) == null)
         {
            jndiEnv.setProperty(Context.OBJECT_FACTORIES, "org.jboss.naming");
         }
      }

      return jndiEnv;
   }
}
