/***************************************
 *                                     *
 *  JBoss: The OpenSource J2EE WebOS   *
 *                                     *
 *  Distributable under LGPL license.  *
 *  See terms of license at gnu.org.   *
 *                                     *
 ***************************************/
package org.jboss.remoting.transport;

import org.jboss.logging.Logger;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.InvokerRegistry;
import org.jboss.remoting.ServerInvocationHandler;
import org.jboss.remoting.ServerInvoker;
import org.jboss.remoting.marshal.MarshallLoaderFactory;
import org.jboss.remoting.marshal.MarshalFactory;
import org.jboss.mx.util.MBeanServerLocator;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * Connector is an implementation of the ConnectorMBean interface.
 *
 * The Connector is root component for the remoting server.  It binds the server transport, marshaller,
 * and handler together to form the remoting server instance.
 *
 * A transport connector is configured via *-service.xml such as:
 * <code>
 * <?xml version="1.0" encoding="UTF-8"?>
 * <!DOCTYPE server>
 * <server>
 * <!-- NOTE: set this up to the path where your libraries are -->
 * <classpath codebase="lib" archives="*"/>
 * <mbean code="org.jboss.remoting.network.NetworkRegistry"
 *        name="jboss.remoting:service=NetworkRegistry"/>
 *
 * <mbean code="org.jboss.remoting.transport.Connector"
 *        xmbean-dd="org/jboss/remoting/transport/Connector.xml"
 *        name="jboss.remoting:service=Connector,transport=Socket"
 *        display-name="Socket transport Connector">
 *
 * <!-- Can either just specify the InvokerLocator attribute and not the invoker element in the -->
 * <!-- Configuration attribute, or do the full invoker configuration in the in invoker element -->
 * <!-- of the Configuration attribute. -->
 * <!-- Remember that if you do use more than one param on the uri, will have to include as a CDATA, -->
 * <!-- otherwise, parser will complain. -->
 *  <!-- <attribute name="InvokerLocator"><![CDATA[socket://${jboss.bind.address}:8084/?enableTcpNoDelay=false&clientMaxPoolSize=30]]></attribute>-->
 *  <attribute name="Configuration">
 *    <config>
 *       <invoker transport="socket">
 *          <attribute name="numAcceptThreads">1</attribute>
 *          <attribute name="maxPoolSize">303</attribute>
 *          <attribute name="clientMaxPoolSize" isParam="true">304</attribute>
 *          <attribute name="socketTimeout">60000</attribute>
 *          <attribute name="serverBindAddress">${jboss.bind.address}</attribute>
 *          <attribute name="serverBindPort">6666</attribute>
 *          <!--  <attribute name="clientConnectAddress">216.23.33.2</attribute> -->
 *          <!--  <attribute name="clientConnectPort">7777</attribute> -->
 *          <attribute name="enableTcpNoDelay" isParam="true">false</attribute>
 *          <attribute name="backlog">200</attribute>
 *       </invoker>
 *       <handlers>
 *          <handler subsystem="mock">org.jboss.remoting.transport.mock.MockServerInvocationHandler</handler>
 *       </handlers>
 *    </config>
 *  </attribute>
 * </mbean>
 *
 * <mbean code="org.jboss.remoting.detection.multicast.MulticastDetector"
 *        name="jboss.remoting:service=Detector,transport=multicast">
 * <!-- you can specifically bind the detector to a specific IP address here -->
 * <!--  <attribute name="BindAddress">${jboss.bind.address}</attribute> -->
 *       <attribute name="Port">2410</attribute>
 * </mbean>
 *
 * <mbean code="org.jboss.remoting.ClientInvokerAdapter"
 *        xmbean-dd="org/jboss/remoting/ClientInvokerAdapter.xml"
 *        name="jboss.remoting:service=InterceptorAdapter">
 * </mbean>
 * </server>
 * </code>
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:adrian.brock@happeningtimes.com">Adrian Brock</a>
 * @author <a href="mailto:d_jencks@users.sourceforge.net">David Jencks</a>
 * @author <a href="mailto:juha@jboss.org">Juha Lindfors</a>
 * @author <a href="mailto:tom@jboss.org">Tom Elrod</a>
 * @version $Revision: 1.14.8.2 $
 * @jmx.mbean description = "An MBean wrapper around a ServerInvoker."
 * @jboss.xmbean
 */
public class Connector implements MBeanRegistration, ConnectorMBean
{
   protected ServerInvoker invoker;

   private String locatorURI;

   private Element xml;

   private InvokerLocator locator;

   private MBeanServer server;

   private Connector marshallerLoaderConnector = null;
   private boolean isMarshallerLoader = false;

   private boolean isStarted = false;

   protected final Logger log = Logger.getLogger(getClass());

   public Connector()
   {

   }

   protected Connector(boolean isMarshallerConnector)
   {
      this();
      this.isMarshallerLoader = isMarshallerConnector;
   }

   public boolean isStarted()
   {
      return isStarted;
   }

   /**
    * This method is called by the MBeanServer before registration takes
    * place. The MBean is passed a reference of the MBeanServer it is
    * about to be registered with. The MBean must return the ObjectName it
    * will be registered with. The MBeanServer can pass a suggested object
    * depending upon how the MBean is registered.<p>
    * <p/>
    * The MBean can stop the registration by throwing an exception.The
    * exception is forwarded to the invoker wrapped in an
    * MBeanRegistrationException.
    *
    * @param server the MBeanServer the MBean is about to be
    *               registered with.
    * @param name   the suggested ObjectName supplied by the
    *               MBeanServer.
    * @return the actual ObjectName to register this MBean with.
    * @throws Exception for any error, the MBean is not registered.
    */
   public ObjectName preRegister(MBeanServer server, ObjectName name)
         throws Exception
   {
      this.server = server;
      return name;
   }

   /**
    * This method is called by the MBeanServer after registration takes
    * place or when registration fails.
    *
    * @param registrationDone the MBeanServer passes true when the
    *                         MBean was registered, false otherwise.
    */
   public void postRegister(Boolean registrationDone)
   {
}

   /**
    * This method is called by the MBeanServer before deregistration takes
    * place.<p>
    * <p/>
    * The MBean can throw an exception, this will stop the deregistration.
    * The exception is forwarded to the invoker wrapped in
    * an MBeanRegistrationException.
    */
   public void preDeregister()
         throws Exception
   {
   }

   /**
    * This method is called by the MBeanServer after deregistration takes
    * place.
    */
   public void postDeregister()
   {
}

   /**
    * Starts the connector.
    *
    * @jmx.managed-operation description = "Start sets up the ServerInvoker we are wrapping."
    * impact      = "ACTION"
    */
   public void start()
         throws Exception
   {
      if(!isStarted)
      {

         Map invokerConfig = new HashMap();

         if(locatorURI == null)
         {
            // nothing set for the InvokerLocator attribute, check to see if is part of Configuration attribute
            if(xml != null)
            {
               getInvokerConfig(invokerConfig);
            }
         }

         if(locatorURI == null)
         {
            throw new IllegalStateException("Connector not configured with LocatorURI.");
         }

         locator = new InvokerLocator(locatorURI);
         ClassLoader cl = Thread.currentThread().getContextClassLoader();

         if(cl == null)
         {
            cl = getClass().getClassLoader();
         }

         if(invoker == null)
         {
            // create the server invoker
            invoker = InvokerRegistry.createServerInvoker(this.locator, invokerConfig);
            // this can change
            locator = invoker.getLocator();

            // this will set the mbean server on the invoker and register it with mbean server
            if(server != null)
            {
               try
               {
                  server.registerMBean(invoker, new ObjectName(invoker.getMBeanObjectName()));
                  invoker.setMBeanServer(server);
               }
               catch(Throwable e)
               {
                  log.warn("Error registering invoker " + invoker + " with MBeanServer.", e);
               }
            }
         }


         if(!isMarshallerLoader)
         {
            // need to check if should create a marshaller loader on the server side
            if(marshallerLoaderConnector == null)
            {
               marshallerLoaderConnector = createMarshallerLoader(locator);
            }
            if(marshallerLoaderConnector != null && !marshallerLoaderConnector.isStarted())
            {
               marshallerLoaderConnector.start();
            }
         }

         // want to have handlers registered before starting, so if someone makes invocation,
         // there is something to handle it.
         configureHandlers(cl);

         if(invoker.isStarted() == false)
         {
            try
            {
               invoker.start();
            }
            catch(Exception e)
            {
               if(marshallerLoaderConnector != null)
               {
                  marshallerLoaderConnector.stop();
               }
               log.error("Error starting connector.", e);
               throw e;
            }
         }
         isStarted = true;
      }

   }

   private Connector createMarshallerLoader(InvokerLocator locator)
   {
      /**
       * This is a bit of a hack, but have to bootstrap the marshaller/unmarshaller here because
       * need them loaded and added to the MarshalFactory now, because is possible the first client
       * to make a call on this connector may not have the marshaller/unmarshaller.  Therefore, when
       * the MarshallerLoaderHandler goes to load them for the client (MarshallerLoaderClient), they
       * have to be there.  Otherwise, would not be loaded until first client actually reaches the
       * target server invoker, where they would otherwise be loaded.
       */
      MarshalFactory.getMarshaller(locator, this.getClass().getClassLoader());

      Connector marshallerLoader = null;
      InvokerLocator loaderLocator = MarshallLoaderFactory.convertLocator(locator);
      // if loaderLocator is null, then probably not defined to have loader service (i.e. no loader port specified)
      if( loaderLocator != null )
      {
         marshallerLoader = MarshallLoaderFactory.createMarshallLoader(loaderLocator);
      }
      return marshallerLoader;
   }

   private void getInvokerConfig(Map invokerConfig)
   {
      try
      {
         NodeList invokerNodes = xml.getElementsByTagName("invoker");

         if(invokerNodes != null && invokerNodes.getLength() >= 1)
         {
            // only accept on invoker per connector at present
            Node invokerNode = invokerNodes.item(0);

            NamedNodeMap attributes = invokerNode.getAttributes();
            Node transportNode = attributes.getNamedItem("transport");

            if(transportNode != null)
            {
               String transport = transportNode.getNodeValue();

               // need to log warning if there are more than one invoker elements
               if(invokerNodes.getLength() > 1)
               {
                  log.warn("Found more than one invokers defined in configuration.  " +
                           "Will only be using the first one - " + transport);
               }

               // now create a map for all the sub attributes
               Map paramConfig = new HashMap();
               NodeList invokerAttributes = invokerNode.getChildNodes();
               int len = invokerAttributes.getLength();
               for(int x = 0; x < len; x++)
               {
                  Node attr = invokerAttributes.item(x);
                  if("attribute".equals(attr.getNodeName()))
                  {
                     String name = attr.getAttributes().getNamedItem("name").getNodeValue();
                     String value = attr.getFirstChild().getNodeValue();
                     invokerConfig.put(name, value);
                     Node isParamAttribute = attr.getAttributes().getNamedItem("isParam");
                     if(isParamAttribute != null && Boolean.valueOf(isParamAttribute.getNodeValue()).booleanValue())
                     {
                        paramConfig.put(name, value);
                     }
                  }
               }

               // should now have my map with all my attributes, now need to look for
               // specific attributes that will impact the locator uri.
               String clientConnectAddress = (String) invokerConfig.get("clientConnectAddress");
               String clientConnectPort = (String) invokerConfig.get("clientConnectPort");
               String serverBindAddress = (String) invokerConfig.get("serverBindAddress");
               String serverBindPort = (String) invokerConfig.get("serverBindPort");

               String host = clientConnectAddress != null ? clientConnectAddress : serverBindAddress != null ? serverBindAddress : InetAddress.getLocalHost().getHostAddress();
               int port = clientConnectPort != null ? Integer.parseInt(clientConnectPort) : serverBindPort != null ? Integer.parseInt(serverBindPort) : PortUtil.findFreePort();

               if("0.0.0.0".equals(host))
               {
                  host = InetAddress.getLocalHost().getHostAddress();
               }

               // finally, let's bild the invoker uri
               String tempURI = transport + "://" + host + ":" + port;

               // an params to add to the uri?
               if(paramConfig.size() > 0)
               {
                  tempURI += "/?";
                  Iterator keyItr = paramConfig.keySet().iterator();
                  if(keyItr.hasNext())
                  {
                     Object name = keyItr.next();
                     Object value = paramConfig.get(name);
                     tempURI += name + "=" + value;
                  }
                  while(keyItr.hasNext())
                  {
                     tempURI += "&";
                     Object name = keyItr.next();
                     Object value = paramConfig.get(name);
                     tempURI += name + "=" + value;
                  }
               }
               locatorURI = tempURI;
            }
            else
            {
               log.error("Invoker element within Configuration attribute does not contain a transport attribute.");
            }

         }
      }
      catch(Exception e)
      {
         log.error("Error configuring invoker for connector.", e);
         throw new IllegalStateException("Error configuring invoker for connector.  Can not continue without invoker.");
      }
}

   private void configureHandlers(ClassLoader cl)
         throws Exception
   {
      if(xml != null)
      {
         NodeList handlersNodes = xml.getElementsByTagName("handler");

         if(handlersNodes == null || handlersNodes.getLength() <= 0)
         {
            throw new IllegalArgumentException("required 'handler' element not found");
         }

         int len = handlersNodes.getLength();

         for(int c = 0; c < len; c++)
         {
            Node node = handlersNodes.item(c);
            Node subNode = node.getAttributes().getNamedItem("subsystem");

            if(subNode == null)
            {
               throw new IllegalArgumentException("Required 'subsystem' attribute on 'handler' element");
            }

            String handlerClass = node.getFirstChild().getNodeValue();

            boolean isObjName = false;
            ServerInvocationHandler handler = null;

            //first check to see if this is an ObjectName
            try
            {
               ObjectName objName = new ObjectName(handlerClass);
               handler = createHandlerProxy(objName);
               isObjName = true;
            }
            catch(MalformedObjectNameException e)
            {
               log.debug("Handler supplied is not an object name.");
            }

            if(!isObjName)
            {
               handler = (ServerInvocationHandler) cl.loadClass(handlerClass).newInstance();
            }

            StringTokenizer tok = new StringTokenizer(subNode.getNodeValue(), ",");

            while(tok.hasMoreTokens())
            {
               String subsystem = tok.nextToken();
               addInvocationHandler(subsystem, handler);
            }
         }
      }
   }

   private ServerInvocationHandler createHandlerProxy(ObjectName objName)
   {
      ServerInvocationHandler handler;
      if(server != null)
      {
         handler = (ServerInvocationHandler)
               MBeanServerInvocationHandler.newProxyInstance(server,
                                                             objName,
                                                             ServerInvocationHandler.class,
                                                             false);
      }
      else
      {
         throw new RuntimeException("Can not register MBean invocation handler as the Connector has not been registered with a MBeanServer.");
      }
      return handler;
   }

   /**
    * Stops the connector.
    *
    * @jmx.managed-operation description = "Stop tears down the ServerInvoker we are wrapping."
    * impact      = "ACTION"
    */
   public void stop()
   {
      if(isStarted)
      {
         if(invoker != null)
         {
            invoker.stop();
            invoker = null;
         }
         if(marshallerLoaderConnector != null && marshallerLoaderConnector.isStarted)
         {
            marshallerLoaderConnector.stop();
            marshallerLoaderConnector = null;
         }
         isStarted = false;
      }
}

   /**
    * Creates the connector.
    *
    * @jmx.managed-operation
    */
   public void create()
         throws Exception
   {
   }

   /**
    * Destroys the connector.
    *
    * @jmx.managed-operation
    */
   public void destroy()
   {
       if (invoker!=null)
       {
          invoker.destroy();
          invoker=null;
       }
}

   /**
    * Returns the locator to the connector. Locator is the actual InvokerLocator
    * object used to identify and get the ServerInvoker we are wrapping.
    *
    * @jmx.managed-attribute description = "Locator is the actual InvokerLocator object used to
    * identify and get the ServerInvoker we are wrapping."
    * access      = "read-only"
    */
   public InvokerLocator getLocator()
   {
      return locator;
}

   /**
    * Sets the invoker locator. InvokerLocator is the string URI representation
    * of the InvokerLocator used to get and identify the ServerInvoker
    * we are wrapping.
    *
    * @jmx.managed-attribute description = "InvokerLocator is the string URI representation of the
    * InvokerLocator used to get and identify the ServerInvoker
    * we are wrapping."
    * access     = "read-write"
    */
   public void setInvokerLocator(String locator)
         throws Exception
   {
      locatorURI = locator;
   }


   /**
    * Returns the invoker locator. InvokerLocator is the string URI representation
    * of the InvokerLocator used to get and identify the ServerInvoker
    * we are wrapping.
    *
    * @jmx.managed-attribute
    */
   public String getInvokerLocator() throws Exception
   {
      return locatorURI;
   }

   /**
    * Configuration is an xml element indicating subsystems to be registered
    * with the ServerInvoker we wrap. Using mbean subsystems that call
    * registerSubsystem is more flexible.
    *
    * @jmx.managed-attribute description = "Configuration is an xml element indicating subsystems
    * to be registered with the ServerInvoker we wrap. Using
    * mbean subsystems that call registerSubsystem is more
    * flexible."
    * access     = "read-write"
    */
   public void setConfiguration(Element xml)
         throws Exception
   {
      this.xml = xml;
   }

   /**
    * Configuration is an xml element indicating subsystems to be registered
    * with the ServerInvoker we wrap. Using mbean subsystems that call
    * registerSubsystem is more flexible.
    *
    * @jmx.managed-attribute
    */
   public Element getConfiguration()
   {
      return xml;
   }

   /**
    * Adds a handler to the connector via OjbectName.  This will create a mbean proxy of
    * type of ServerInvocationHandler for the MBean specified by object name passed (so has
    * to implement ServerInvocationHandler interface).
    *
    * @param subsystem
    * @param handlerObjectName
    * @throws Exception
    * @jmx.managed-operation description = "Add a subsystem invocation handler to the ServerInvoker
    * we wrap, identified by the subsystem parameter."
    * impact      = "ACTION"
    * @jmx.managed-parameter name        = "subsystem"
    * type        = "java.lang.String"
    * description = "The subsystem this handler is for."
    * @jmx.managed-parameter name        = "handlerObjectName"
    * type        = "javax.management.ObjectName"
    * description = "The ServerInvocationHandler MBean we are registering
    * for the subsystem"
    */
   public void addInvocationHandler(String subsystem, ObjectName handlerObjectName) throws Exception
   {
      ServerInvocationHandler invocationHandler = createHandlerProxy(handlerObjectName);
      addInvocationHandler(subsystem, invocationHandler);
   }

   /**
    * Adds an invocation handler for the named subsystem to the invoker we
    * manage, and sets the mbean server on the invocation handler.
    *
    * @jmx.managed-operation description = "Add a subsystem invocation handler to the ServerInvoker
    * we wrap, identified by the subsystem parameter."
    * impact      = "ACTION"
    * @jmx.managed-parameter name        = "subsystem"
    * type        = "java.lang.String"
    * description = "The subsystem this handler is for."
    * @jmx.managed-parameter name        = "handler"
    * type        = "org.jboss.remoting.ServerInvocationHandler"
    * description = "The ServerInvocationHandler we are registering
    * for the subsystem"
    */
   public void addInvocationHandler(String subsystem, ServerInvocationHandler handler)
         throws Exception
   {
      if(invoker == null)
      {
         throw new IllegalStateException("You may only add handlers when the Connector is started");
      }

      handler.setMBeanServer(server);
      invoker.addInvocationHandler(subsystem, handler);
   }

   /**
    * Removes an invocation handler for the supplied subsystem from the invoker
    * we manage, and unsets the MBeanServer on the handler.
    *
    * @jmx.managed-operation description = "Remove a subsystem invocation handler to the
    * ServerInvoker we wrap, identified by the subsystem
    * parameter."
    * impact      = "ACTION"
    * @jmx.managed-parameter name        = "subsystem"
    * type        = "java.lang.String"
    * description = "The subsystem this handler is for."
    */
   public void removeInvocationHandler(String subsystem) throws Exception
   {
      ServerInvocationHandler handler = invoker.removeInvocationHandler(subsystem);

      if(handler != null)
      {
         handler.setMBeanServer(null);
      }
   }

}