/*
 * 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.ha.jndi;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.rmi.MarshalledObject;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import org.jboss.ha.framework.interfaces.HAPartition;
import org.jboss.ha.jndi.spi.DistributedTreeManager;
import org.jboss.invocation.Invocation;
import org.jboss.invocation.MarshalledInvocation;
import org.jboss.logging.Logger;
import org.jboss.system.ServiceMBeanSupport;
import org.jnp.interfaces.Naming;
import org.jnp.interfaces.NamingContext;

import javax.management.ObjectName;
import javax.net.ServerSocketFactory;

/**
 * Management Bean for the protocol independent HA-JNDI service. This allows the
 * naming service transport layer to be provided by a detached invoker service
 * like ProxyFactoryHA.
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @author <a href="mailto:sacha.labourey@cogito-info.ch">Sacha Labourey</a>
 * @author Scott.Stark@jboss.org
 * @version $Revision: 104583 $
 */
public class DetachedHANamingService
   extends ServiceMBeanSupport
   implements DetachedHANamingServiceMBean
{
   // Constants -----------------------------------------------------
   
   // Attributes ----------------------------------------------------
   /**
    * The jnp server socket through which the HAJNDI stub is vended
    */
   ServerSocket bootstrapSocket;

   /**
    * The Naming interface server implementation
    */
   HAJNDI theServer;
   /**
    * The mapping from the long method hash to the Naming Method
    */
   private Map<Long, Method> marshalledInvocationMapping;
   /**
    * The protocol stub returned to clients by the bootstrap lookup
    */
   Naming stub;
   /**
    * The HAPartition
    */
   protected HAPartition clusterPartition;
   /**
    * The manager of our distributed bindings
    */
   private DistributedTreeManager  distributedTreeManager;

   /**
    * The proxy factory service that generates the Naming stub
    */
   private ObjectName proxyFactory;
   
   /**
    * The local (non-HA) Naming instance. 
    */
   private Naming localNamingInstance;

   /**
    * The interface to bind to. This is useful for multi-homed hosts that want
    * control over which interfaces accept connections.
    */
   InetAddress bindAddress;
   /**
    * The bootstrapSocket listen queue depth
    */
   private int backlog = 50;
   /**
    * The jnp protocol listening port. The default is 1100, the same as the RMI
    * registry default port.
    */
   int port = 1100;

   /**
    * The autodiscovery multicast group
    */
   String adGroupAddress = NamingContext.DEFAULT_DISCOVERY_GROUP_ADDRESS;
   /**
    * The autodiscovery port
    */
   int adGroupPort = NamingContext.DEFAULT_DISCOVERY_GROUP_PORT;
   /**
    * The interface to bind the Multicast socket for autodiscovery to
    */
   InetAddress discoveryBindAddress;
   /** The runable task for discovery request packets */
   private AutomaticDiscovery autoDiscovery = null;
   /** A flag indicating if autodiscovery should be disabled */
   private boolean discoveryDisabled = false;
   /** The autodiscovery Multicast reply TTL */
   int autoDiscoveryTTL = 16;
   /**
    * An optional custom server socket factory for the bootstrap lookup
    */
   private ServerSocketFactory jnpServerSocketFactory;
   /**
    * The class name of the optional custom JNP server socket factory
    */
   private String jnpServerSocketFactoryName;

   /**
    * The thread pool used to handle jnp stub lookup requests
    */
   Executor lookupPool;

   // Public --------------------------------------------------------

   public DetachedHANamingService()
   {
      // for JMX
   }

   /**
    * Expose the Naming service interface mapping as a read-only attribute
    * @return A Map<Long hash, Method> of the Naming interface
    * @jmx:managed-attribute
    */
   public Map<Long, Method> getMethodMap()
   {
      return this.marshalledInvocationMapping;
   }

   public String getPartitionName()
   {
      return this.clusterPartition.getPartitionName();
   }

   public HAPartition getHAPartition()
   {
      return this.clusterPartition;
   }

   public void setHAPartition(HAPartition clusterPartition)
   {
      this.clusterPartition = clusterPartition;
   }
   
   public DistributedTreeManager  getDistributedTreeManager ()
   {
      return this.distributedTreeManager;
   }

   public void setDistributedTreeManager (DistributedTreeManager  distributedTreeManager )
   {
      this.distributedTreeManager = distributedTreeManager;
   }

   public Naming getLocalNamingInstance()
   {
      return localNamingInstance;
   }

   public void setLocalNamingInstance(Naming localNamingInstance)
   {
      this.localNamingInstance = localNamingInstance;
   }

   public ObjectName getProxyFactoryObjectName()
   {
      return this.proxyFactory;
   }

   public void setProxyFactoryObjectName(ObjectName proxyFactory)
   {
      this.proxyFactory = proxyFactory;
   }

   public void setPort(int p)
   {
      this.port = p;
   }

   public int getPort()
   {
      return this.port;
   }

   public String getBindAddress()
   {
      String address = null;
      if (this.bindAddress != null)
      {
         address = this.bindAddress.getHostAddress();
      }
      return address;
   }

   public void setBindAddress(String host) throws java.net.UnknownHostException
   {
      this.bindAddress = InetAddress.getByName(host);
   }

   public int getBacklog()
   {
      return this.backlog;
   }

   public void setBacklog(int backlog)
   {
      if (backlog <= 0)
      {
         backlog = 50;
      }
      this.backlog = backlog;
   }

   public void setDiscoveryDisabled(boolean disable)
   {
      this.discoveryDisabled = disable;
   }

   public boolean getDiscoveryDisabled()
   {
      return this.discoveryDisabled;
   }

   public String getAutoDiscoveryAddress()
   {
      return this.adGroupAddress;
   }

   public void setAutoDiscoveryAddress(String adAddress)
   {
      this.adGroupAddress = adAddress;
   }

   public int getAutoDiscoveryGroup()
   {
      return this.adGroupPort;
   }
   
   public void setAutoDiscoveryGroup(int adGroup)
   {
      this.adGroupPort = adGroup;
   }

   public String getAutoDiscoveryBindAddress()
   {
      return (this.discoveryBindAddress != null) ? this.discoveryBindAddress.getHostAddress() : null;
   }
   
   public void setAutoDiscoveryBindAddress(String address) throws UnknownHostException
   {
      this.discoveryBindAddress = InetAddress.getByName(address);
   }

   public int getAutoDiscoveryTTL()
   {
      return this.autoDiscoveryTTL;
   }

   public void setAutoDiscoveryTTL(int ttl)
   {
      this.autoDiscoveryTTL = ttl;
   }

   public void setJNPServerSocketFactory(String factoryClassName) throws ClassNotFoundException, InstantiationException, IllegalAccessException
   {
      this.jnpServerSocketFactoryName = factoryClassName;
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      Class<?> clazz = loader.loadClass(this.jnpServerSocketFactoryName);
      this.jnpServerSocketFactory = (ServerSocketFactory) clazz.newInstance();
   }

   public void setLookupPool(Executor lookupPool)
   {
      this.lookupPool = lookupPool;
   }
/*
   public void startService(HAPartition haPartition)
      throws Exception
   {
      this.startService();
   }
*/
   @Override
   protected void createService() throws Exception
   {
      if (this.clusterPartition == null)
      {
         throw new IllegalStateException("HAPartition property must be set before starting HAJNDI service");
      }
      
      if (this.distributedTreeManager == null)
      {
         throw new IllegalStateException("DistributedTreeManager property must be set before starting HAJNDI service");
      }
      
      this.log.debug("Initializing HAJNDI server on partition: " + this.clusterPartition.getPartitionName());
      
      // Start HAJNDI service
      this.theServer = new HAJNDI(this.clusterPartition, this.distributedTreeManager, localNamingInstance);

      // Build the Naming interface method map
      Map<Long, Method> map = new HashMap<Long, Method>(13);
      Method[] methods = Naming.class.getMethods();
      for (Method method: methods)
      {
         Long hash = new Long(MarshalledInvocation.calculateHash(method));
         map.put(hash, method);
      }
      this.marshalledInvocationMapping = Collections.unmodifiableMap(map);
      
      // share instance for in-vm discovery
      NamingContext.setHANamingServerForPartition(this.clusterPartition.getPartitionName(), this.theServer);
   }

   @Override
   protected void startService()
      throws Exception
   {
      this.log.debug("Obtaining the HAJNDI transport proxy");
      this.stub = this.getNamingProxy();
      this.distributedTreeManager.setHAStub(this.stub);
      if (this.port >= 0)
      {
         this.log.debug("Starting HAJNDI bootstrap listener");
         this.initBootstrapListener();
      }

      // Automatic Discovery for unconfigured clients
      if (this.adGroupAddress != null && this.discoveryDisabled == false)
      {
         try
         {
            this.autoDiscovery = new AutomaticDiscovery();
            this.autoDiscovery.start();
            this.lookupPool.execute(this.autoDiscovery);
         }
         catch (Exception e)
         {
            this.log.warn("Failed to start AutomaticDiscovery", e);
         }
      }
      this.log.debug("initializing HAJNDI");
      this.theServer.init();
   }

   @Override
   protected void stopService() throws Exception
   {
      // un-share instance for in-vm discovery
      NamingContext.removeHANamingServerForPartition(this.clusterPartition.getPartitionName());

      // Stop listener
      ServerSocket s = this.bootstrapSocket;
      this.bootstrapSocket = null;
      if (s != null)
      {
         this.log.debug("Closing the HAJNDI bootstrap listener");
         s.close();
      }

      // Stop HAJNDI service
      this.log.debug("Stopping the HAJNDI service");
      this.theServer.shutdown();

      this.log.debug("Stopping AutomaticDiscovery");
      if (this.autoDiscovery != null && this.discoveryDisabled == false)
      {
         this.autoDiscovery.stop();
      }
   }
   
   @Override
   protected void destroyService() throws Exception
   {
      this.log.debug("Destroying the HAJNDI service");      
   }

   /**
    * Expose the Naming service via JMX to invokers.
    * @param invocation A pointer to the invocation object
    * @return Return value of method invocation.
    * @throws Exception Failed to invoke method.
    * @jmx:managed-operation
    */
   public Object invoke(Invocation invocation) throws Exception
   {
      // Set the method hash to Method mapping
      if (invocation instanceof MarshalledInvocation)
      {
         MarshalledInvocation mi = (MarshalledInvocation) invocation;
         mi.setMethodMap(this.marshalledInvocationMapping);
      }
      // Invoke the Naming method via reflection
      Method method = invocation.getMethod();
      Object[] args = invocation.getArguments();
      Object value = null;
      try
      {
         value = method.invoke(this.theServer, args);
      }
      catch (InvocationTargetException e)
      {
         Throwable t = e.getTargetException();
         if (t instanceof Exception)
         {
            throw (Exception) t;
         }

         throw new UndeclaredThrowableException(t, method.toString());
      }

      return value;
   }

   /**
    * Bring up the bootstrap lookup port for obtaining the naming service proxy
    */
   protected void initBootstrapListener()
   {
      // Start listener
      try
      {
         // Get the default ServerSocketFactory is one was not specified
         if (this.jnpServerSocketFactory == null)
         {
            this.jnpServerSocketFactory = ServerSocketFactory.getDefault();
         }
         this.bootstrapSocket = this.jnpServerSocketFactory.createServerSocket(this.port, this.backlog, this.bindAddress);
         // If an anonymous port was specified get the actual port used
         if (this.port == 0)
         {
            this.port = this.bootstrapSocket.getLocalPort();
         }
         String msg = "Started HAJNDI bootstrap; jnpPort=" + this.port
            + ", backlog=" + this.backlog + ", bindAddress=" + this.bindAddress;
         this.log.info(msg);
      }
      catch (IOException e)
      {
         this.log.error("Could not start HAJNDI bootstrap listener on port " + this.port, e);
      }

      if (lookupPool == null)
      {
         throw new IllegalArgumentException("DetachedHANamingService started up without a thread pool");
      }
      AcceptHandler handler = new AcceptHandler();
      lookupPool.execute(handler);
   }

   // Protected -----------------------------------------------------

   /**
    * Get the Naming proxy for the transport. This version looks  up the
    * proxyFactory service Proxy attribute. Subclasses can override this to set
    * the proxy another way.
    * @return The Naming proxy for the protocol used with the HAJNDI service
    */
   protected Naming getNamingProxy() throws Exception
   {
      return (Naming) this.server.getAttribute(this.proxyFactory, "Proxy");
   }

   // Private -------------------------------------------------------

   private class AutomaticDiscovery
      implements Runnable
   {
      private Logger log = Logger.getLogger(AutomaticDiscovery.class);
      /** The socket for auto discovery requests */
      private MulticastSocket socket = null;
      /** The ha-jndi addres + ':' + port string */
      private byte[] ipAddress = null;
      /** The multicast group address */
      private InetAddress group = null;
      private volatile boolean stopping = false;
      // Thread that is executing the run() method
      private volatile Thread receiverThread = null;
      private volatile boolean receiverStopped = true;

      public AutomaticDiscovery() throws Exception
      {
      }

      public void start() throws Exception
      {
         this.stopping = false;
         
         // Set up the multicast socket on which we listen for discovery requests
         
         this.group = InetAddress.getByName(DetachedHANamingService.this.adGroupAddress);
         
         // On Linux, we avoid cross-talk problem by binding the MulticastSocket
         // to the multicast address. See https://jira.jboss.org/jira/browse/JGRP-777
         if(checkForPresence("os.name", "linux"))
         {             
            this.socket = createMulticastSocket(group, DetachedHANamingService.this.adGroupPort);
         }
         else
         {
            this.socket = new MulticastSocket(DetachedHANamingService.this.adGroupPort);
         }
         
         // If there is a valid bind address, set the socket interface to it         
         // Use the jndi bind address if there is no discovery address
         if (DetachedHANamingService.this.discoveryBindAddress == null)
         {
            DetachedHANamingService.this.discoveryBindAddress = DetachedHANamingService.this.bindAddress;
         }
         
         if (DetachedHANamingService.this.discoveryBindAddress != null 
               && DetachedHANamingService.this.discoveryBindAddress.isAnyLocalAddress() == false)
         {
            this.socket.setInterface(DetachedHANamingService.this.discoveryBindAddress);
         }
         
         this.socket.setTimeToLive(DetachedHANamingService.this.autoDiscoveryTTL);
         this.socket.joinGroup(this.group);

         
         // Determine the hostname:port string we will return to discovery requests
         String address = DetachedHANamingService.this.getBindAddress();
         /* An INADDR_ANY (0.0.0.0 || null) address is useless as the value
            sent to a remote client so check for this and use the local host
            address instead.
          */
         if (address == null || address.equals("0.0.0.0"))
         {
            address = InetAddress.getLocalHost().getHostAddress();
         }
         this.ipAddress = (address + ":" + DetachedHANamingService.this.port).getBytes();

         this.log.info("Listening on " + this.socket.getInterface() + ":" + this.socket.getLocalPort()
            + ", group=" + DetachedHANamingService.this.adGroupAddress
            + ", HA-JNDI address=" + new String(this.ipAddress));
      }

      public void stop()
      {
         try
         {
            this.stopping = true;
            
            // JBAS-2834 -- try to stop the receiverThread
            if (this.receiverThread != null
                  && this.receiverThread != Thread.currentThread()
                  && this.receiverThread.isInterrupted() == false)
            {
               // Give it a moment to die on its own (unlikely)
               this.receiverThread.join(5);
               if (!this.receiverStopped)
               {
                  this.receiverThread.interrupt(); // kill it
               }
            }
            
            this.socket.leaveGroup(this.group);
            this.socket.close();
         }
         catch (Exception ex)
         {
            this.log.error("Stopping AutomaticDiscovery failed", ex);
         }
      }

      public void run()
      {
         boolean trace = this.log.isTraceEnabled();
         this.log.debug("Discovery request thread begin");
         
         // JBAS-2834 Cache a reference to this thread so stop()
         // can interrupt it if necessary
         this.receiverThread = Thread.currentThread();

         this.receiverStopped = false;
         
         // Wait for a datagram
         while (true)
         {
            // Stopped by normal means
            if (this.stopping)
            {
               break;
            }
            try
            {
               if (trace)
               {
                  this.log.trace("HA-JNDI AutomaticDiscovery waiting for queries...");
               }
               byte[] buf = new byte[256];
               DatagramPacket packet = new DatagramPacket(buf, buf.length);
               this.socket.receive(packet);
               if (trace)
               {
                  this.log.trace("HA-JNDI AutomaticDiscovery Packet received.");
               }

               // Queue the response to the thread pool
               DiscoveryRequestHandler handler = new DiscoveryRequestHandler(this.log,
                  packet, this.socket, this.ipAddress);
               lookupPool.execute(handler);
               if (trace)
               {
                  this.log.trace("Queued DiscoveryRequestHandler");
               }
            }
            catch (Throwable t)
            {
               if (this.stopping == false)
               {
                  this.log.warn("Ignored error while processing HAJNDI discovery request:", t);
               }
            }
         }
         this.receiverStopped = true;
         this.log.debug("Discovery request thread end");
      }

      private boolean checkForPresence(final String key, String value)
      {
         try
         {
            String tmp = null;
            if (System.getSecurityManager() == null)
            {
               tmp = System.getProperty(key);
            }
            else
            {
               // Use a different local var to limit scope of @SuppressWarnings
               @SuppressWarnings("unchecked")
               String prop = (String) AccessController.doPrivileged(new PrivilegedAction()
                  {
                     public Object run()
                     {
                        return System.getProperty(key);
                     }
   
                  });
               tmp = prop;
            }

            return tmp != null && tmp.trim().toLowerCase().startsWith(value);
         }
         catch (Throwable t)
         {
            return false;
         }
      }
      
      private MulticastSocket createMulticastSocket(InetAddress mcast_addr, int port) throws IOException 
      {
        if(mcast_addr != null && !mcast_addr.isMulticastAddress()) {
            log.warn("mcast_addr (" + mcast_addr + ") is not a multicast address, will be ignored");
            return new MulticastSocket(port);
        }

        SocketAddress saddr=new InetSocketAddress(mcast_addr, port);
        MulticastSocket retval=null;

        try {
            retval=new MulticastSocket(saddr);
        }
        catch(IOException ex) {
            StringBuilder sb=new StringBuilder();
             String type=mcast_addr != null ? mcast_addr instanceof Inet4Address? "IPv4" : "IPv6" : "n/a";
             sb.append("could not bind to " + mcast_addr + " (" + type + " address)");
             sb.append("; make sure your mcast_addr is of the same type as the IP stack (IPv4 or IPv6).");
             sb.append("\nWill ignore mcast_addr, but this may lead to cross talking " +
                     "(see http://www.jboss.com/wiki/Edit.jsp?page=CrossTalking for details). ");
             sb.append("\nException was: " + ex);
             log.warn(sb);
        }
        if(retval == null)
        {
            retval=new MulticastSocket(port);
        }
        return retval;
    }
   }

   /**
    * The class used as the runnable for writing the bootstrap stub
    */
   private class DiscoveryRequestHandler implements Runnable
   {
      private Logger log;
      private MulticastSocket socket;
      private DatagramPacket packet;
      private byte[] ipAddress;

      DiscoveryRequestHandler(Logger log, DatagramPacket packet,
         MulticastSocket socket, byte[] ipAddress)
      {
         this.log = log;
         this.packet = packet;
         this.socket = socket;
         this.ipAddress = ipAddress;
      }
      public void run()
      {
         boolean trace = this.log.isTraceEnabled();
         if( trace )
         {
            this.log.trace("DiscoveryRequestHandler begin");
         }
         // Return the naming server IP address and port to the client
         try
         {
            // See if the discovery is restricted to a particular parition
            String requestData = new String(this.packet.getData()).trim();
            if( trace )
            {
               this.log.trace("RequestData: "+requestData);
            }
            int colon = requestData.indexOf(':');
            if (colon > 0)
            {
               // Check the partition name
               String name = requestData.substring(colon + 1);
               if (name.equals(DetachedHANamingService.this.clusterPartition.getPartitionName()) == false)
               {
                  this.log.debug("Ignoring discovery request for partition: " + name);
                  if( trace )
                  {
                     this.log.trace("DiscoveryRequestHandler end");
                  }
                  return;
               }
            }
            DatagramPacket p = new DatagramPacket(this.ipAddress, this.ipAddress.length,
               this.packet.getAddress(), this.packet.getPort());
            if (trace)
            {
               this.log.trace("Sending AutomaticDiscovery answer: " + new String(this.ipAddress) +
                         " to " + this.packet.getAddress() + ":" + this.packet.getPort());
            }
            this.socket.send(p);
            if (trace)
            {
               this.log.trace("AutomaticDiscovery answer sent.");
            }
         }
         catch (IOException ex)
         {
            this.log.error("Error writing response", ex);
         }
         if( trace )
         {
            this.log.trace("DiscoveryRequestHandler end");
         }
      }
   }

   /**
    * The class used as the runnable for the bootstrap lookup thread pool.
    */
   private class AcceptHandler implements Runnable
   {
      AcceptHandler()
      {
      }
      
      public void run()
      {
         boolean trace = DetachedHANamingService.this.log.isTraceEnabled();
         while (DetachedHANamingService.this.bootstrapSocket != null)
         {
            Socket socket = null;
            // Accept a connection
            try
            {
               socket = DetachedHANamingService.this.bootstrapSocket.accept();
               if (trace)
               {
                  DetachedHANamingService.this.log.trace("Accepted bootstrap client: "+socket);
               }
               BootstrapRequestHandler handler = new BootstrapRequestHandler(socket);
               lookupPool.execute(handler);
            }
            catch (IOException e)
            {
               // Stopped by normal means
               if (DetachedHANamingService.this.bootstrapSocket == null)
               {
                  return;
               }
               DetachedHANamingService.this.log.error("Naming accept handler stopping", e);
            }
            catch(Throwable e)
            {
               DetachedHANamingService.this.log.error("Unexpected exception during accept", e);
            }
         }
      }
   }

   /**
    * The class used as the runnable for writing the bootstrap stub
    */
   private class BootstrapRequestHandler implements Runnable
   {
      private Socket socket;
      BootstrapRequestHandler(Socket socket)
      {
         this.socket = socket;
      }
      public void run()
      {
         // Return the naming server stub
         try
         {
            OutputStream os = this.socket.getOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(os);
            MarshalledObject replyStub = new MarshalledObject(DetachedHANamingService.this.stub);
            out.writeObject(replyStub);
            out.close();
         }
         catch (IOException ex)
         {
            DetachedHANamingService.this.log.debug("Error writing response to " + this.socket, ex);
         }
         finally
         {
            try
            {
               this.socket.close();
            }
            catch (IOException e)
            {
            }
         }
      }
   }
}
