/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.remoting.detection.jndi;

import org.jboss.logging.Logger;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.InvokerRegistry;
import org.jboss.remoting.detection.AbstractDetector;
import org.jboss.remoting.detection.Detection;
import org.jboss.remoting.ident.Identity;
import org.jboss.remoting.transport.PortUtil;
import org.jnp.interfaces.NamingContextFactory;
import org.jnp.server.Main;

import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import java.net.InetAddress;
import java.util.Map;
import java.util.Properties;

/**
 * This is a remoting detector for the remoting package which uses a JNDI server to
 * maintain the registeries for remote invoker servers (stored as Detection messages).
 * This detector is intended to be used in conjuntion with an external JNDI server that
 * is already running.  This is done by passing all the information needed to connect
 * to the remote JNDI server via the setter methods.  This can also be done within
 * the jboss-service.xml.  An example of the entry is as follows:<p>
 * &lt;mbean code="org.jboss.remoting.detection.jndi.JNDIDetector" name="jboss.remoting:service=Detector,transport=jndi"&gt;<br>
 * &lt;attribute name="Port"&gt;5555&lt;/attribute&gt;<br>
 * &lt;attribute name="Host"&gt;foo.bar.com&lt;/attribute&gt;<br>
 * &lt;attribute name="ContextFactory"&gt;org.jnp.interfaces.NamingContextFactory&lt;/attribute&gt;<br>
 * &lt;attribute name="URLPackage"&gt;org.jboss.naming:org.jnp.interfaces&lt;/attribute&gt;<br>
 * &lt;/mbean&gt;<br><p>
 * Note: The above xml is for the JBoss JNP JNDI server, and has not be tested (just an example).<p>
 * Be aware that just because this detector is stopped (and the entry removed from the JNDI server)
 * remote JNDIDetectors may not recognize that the invoker servers are not available.  This is because
 * once remote invoker servers (connectors) are detected, they will be pinged directly to determine
 * if they are no longer available.  However, no new JNDIDetectors will detect your server once stopped.
 * Also, please note that currently the detection registries are bound at the root context and
 * not a sub context (which is on the todo list, but you know how that goes).<p>
 * Important to also note that if any of the above attributes are set once the detector has
 * started, they will not be used in connecting to the JNDI server until the detector is stopped
 * and re-started (they do not change the JNDI server connection dynamically).<p>
 *
 * @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
 */
public class JNDIDetector extends AbstractDetector implements JNDIDetectorMBean
{
   private int port;
   private String host;
   private String contextFactory = NamingContextFactory.class.getName();
   ;
   private String urlPackage = "org.jboss.naming:org.jnp.interfaces";
   ;

   private Identity id;
   private Context context;
   private int cleanDetectionCount = 0;

   public static final String DETECTION_SUBCONTEXT_NAME = "detection";

   private String subContextName = DETECTION_SUBCONTEXT_NAME;

   /**
    * Indicates the number of time will detect before doing check to see if server still alive.
    */
   private int detectionNumber = 5;

   protected final Logger log = Logger.getLogger(getClass());
   
   public JNDIDetector()
   {
   }
   
   public JNDIDetector(Map config)
   {
      super(config);
   }
   
   /**
    * Gets the port used to connect to the JNDI Server.
    *
    * @return
    */
   public int getPort()
   {
      return port;
   }

   /**
    * Sets the port to use when connecting to JNDI server
    *
    * @param port
    */
   public void setPort(int port)
   {
      this.port = port;
   }

   /**
    * Gets the host to use when connecting to JNDI server
    *
    * @return
    */
   public String getHost()
   {
      return host;
   }

   /**
    * Sets the host to use when connecting to JNDI server
    *
    * @param host
    */
   public void setHost(String host)
   {
      this.host = host;
   }

   /**
    * The context factory string used when connecting to the JNDI server
    *
    * @return
    */
   public String getContextFactory()
   {
      return contextFactory;
   }

   /**
    * Sets the sub context name under which detection messages will be bound
    * and looked up.
    * @param subContextName
    */
   public void setSubContextName(String subContextName)
   {
      this.subContextName = subContextName;
   }

   /**
    * Gets the sub context name under which detection messages will be bound and
    * looked up.
    * @return
    */
   public String getSubContextName()
   {
      return this.subContextName;
   }

   /**
    * The context factory string to use when connecting to the JNDI server.
    * Should be a qualified class name for JNDI client.
    *
    * @param contextFactory
    */
   public void setContextFactory(String contextFactory)
   {
      this.contextFactory = contextFactory;
   }

   /**
    * The url package string used when connecting to JNDI server
    *
    * @return
    */
   public String getURLPackage()
   {
      return urlPackage;
   }

   /**
    * The url package string to use when connecting to the JNDI server.
    *
    * @param urlPackage
    */
   public void setURLPackage(String urlPackage)
   {
      this.urlPackage = urlPackage;
   }

   /**
    * Will establish the connection to the JNDI server and start detection of other servers.
    *
    * @throws Exception
    */
   public void start() throws Exception
   {
      createContext();
      id = Identity.get(mbeanserver);
      super.start();
   }

   /**
    * Creates connection to JNDI server (which should have already happened in start()
    * method) and will begin checking for remote servers as well as registering itself
    * so will be visible by remote detectors.
    */
   protected void heartbeat()
   {
      try
      {
         //Need to establish connection to server
         if(context == null)
         {
            createContext();
         }
         checkRemoteDetectionMsg();
      }
      catch(NamingException nex)
      {
         log.error("Can not connect to JNDI server to register local connectors.", nex);
      }
   }

   protected void forceHeartbeat()
   {
      heartbeat();
   }

   /**
    * Gets the number of detection iterations before manually pinging remote
    * server to make sure still alive.
    *
    * @return
    */
   public int getCleanDetectionNumber()
   {
      return detectionNumber;
   }

   /**
    * Sets the number of detection iterations before manually pinging remote
    * server to make sure still alive.  This is needed since remote server
    * could crash and yet still have an entry in the JNDI server, thus
    * making it appear that it is still there.
    *
    * @param cleanDetectionNumber
    */
   public void setCleanDetectionNumber(int cleanDetectionNumber)
   {
      detectionNumber = cleanDetectionNumber;
   }

   private void checkRemoteDetectionMsg()
   {
      try
      {
         boolean localFound = false;
         cleanDetectionCount++;
         boolean cleanDetect = cleanDetectionCount > detectionNumber;
         String bindName = "";
         NamingEnumeration enumeration = context.listBindings(bindName);
         while(enumeration.hasMore())
         {
            Binding binding = (Binding) enumeration.next();
            Detection regMsg = (Detection) binding.getObject();
            // No need to detect myself here
            if(isRemoteDetection(regMsg))
            {
               log.debug("Detected id: " + regMsg.getIdentity().getInstanceId() + ", message: " + regMsg);

               if(cleanDetect)
               {
                  if(log.isTraceEnabled())
                  {
                     log.trace("Doing clean detection.");
                  }
                  // Need to actually detect if servers registered in JNDI server
                  // are actually there (since could die before unregistering)
                  ClassLoader cl = JNDIDetector.this.getClass().getClassLoader();
                  if(!checkInvokerServer(regMsg, cl))
                  {
                     unregisterDetection(regMsg.getIdentity().getInstanceId());
                  }
               }
               else
               {
                  // Let parent handle detection
                  detect(regMsg);
               }
            }
            else
            {
               //verify local detection message is correct
               if(!verifyLocalDetectionMsg(regMsg))
               {
                  addLocalDetectionMsg();
               }
               localFound = true;
            }
         }
         if(cleanDetect)
         {
            // did clean detect, now need to reset.
            cleanDetectionCount = 0;
         }
         if(!localFound)
         {
            // never found local detection message in list, so add it
            addLocalDetectionMsg();
         }
      }
      catch(NamingException e)
      {
         log.error("Exception getting detection messages from JNDI server.", e);
      }
   }

   private boolean verifyLocalDetectionMsg(Detection regMsg) throws NamingException
   {
      boolean verified = false;

      InvokerLocator[] locators = InvokerRegistry.getRegisteredServerLocators();
      Detection msg = createDetection();
      String sId = id.getInstanceId();
      InvokerLocator[] invokers = regMsg.getLocators();

      // first do sanity check to make sure even local detection msg (just in case)
      if(sId.equals(regMsg.getIdentity().getInstanceId()))
      {

         // now see if invoker list changed
         boolean changed = false;
         if(locators.length != invokers.length)
         {
            changed = true;
         }
         else
         {
            // now need to make sure all the invokers are same now as in old detection msg
            // not the most efficient (or elegant) way to do this, but list is short
            boolean found = false; // flag for if current invoker in list found in old list
            for(int i = 0; i < locators.length; i++)
            {
               found = false;
               for(int x = 0; x < invokers.length; x++)
               {
                  if(locators[i].equals(invokers[x]))
                  {
                     found = true;
                     break;
                  }
               }
               if(!found)
               {
                  break;
               }
            }
            if(!found)
            {
               changed = true;
            }
         }
         if(changed)
         {
            registerDetectionMsg(sId, msg);
         }
         // are sure that local detection is correct in JNDI server now
         verified = true;
      }
      return verified;
   }

   private void addLocalDetectionMsg() throws NamingException
   {
      Detection msg = createDetection();
      String sId = id.getInstanceId();
      registerDetectionMsg(sId, msg);
   }

   private void registerDetectionMsg(String sId, Detection msg) throws NamingException
   {
      if(sId != null && msg != null)
      {
         try
         {
            context.bind(sId, msg);
            log.info("Added " + sId + " to registry.");
         }
         catch(NameAlreadyBoundException nabex)
         {
            if(log.isTraceEnabled())
            {
               log.trace(sId + " already bound to server.");
            }
         }
      }
   }

   /**
    * Convience method to see if given proper configuration to connect to an
    * existing JNDI server.  If not, will create one via JBoss JNP.  Should
    * really only be needed for testing.
    */
   private void verifyJNDIServer()
   {
      if(host == null || host.length() == 0)
      {
         try
         {
            log.info("JNDI Server configuration information not present so will create a local server.");
            host = InetAddress.getLocalHost().getHostName();
            port = PortUtil.findFreePort(host);

            log.info("Remoting JNDI detector starting JNDI server instance since none where specified via configuration.");
            log.info("Remoting JNDI server started on host + " + host + " and port " + port);

            //If no server information provided, then start one of our own by default
            Main server = new Main();
            server.setPort(port);
            server.setBindAddress(host);
            server.start();

            contextFactory = NamingContextFactory.class.getName();
            urlPackage = "org.jboss.naming:org.jnp.interfaces";
         }
         catch(Exception e)
         {
            log.error("Error starting up JNDI server since none was specified via configuration.", e);
         }
      }
   }

   /**
    * Will try to establish the initial context to the JNDI server based
    * on the configuration properties set.
    *
    * @throws NamingException
    */
   private void createContext() throws NamingException
   {
      verifyJNDIServer();

      Properties env = new Properties();

      env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
      env.put(Context.PROVIDER_URL, host + ":" + port);
      env.put(Context.URL_PKG_PREFIXES, urlPackage);

      InitialContext initialContext = new InitialContext(env);
      try
      {
         context = (Context) initialContext.lookup(subContextName);
      }
      catch(NamingException e)
      {
         try
         {
            context = initialContext.createSubcontext(subContextName);
         }
         catch(NameAlreadyBoundException e1)
         {
            log.debug("The sub context " + subContextName + " was created before we could.");
            context = (Context) initialContext.lookup(subContextName);
         }
      }
   }

   public void stop() throws Exception
   {
      try
      {
         super.stop();
      }
      finally // Need to cleanup JNDI, even if super's stop throws exception
      {
         String sId = id.getInstanceId();
         try
         {
            unregisterDetection(sId);
         }
         catch(NamingException e)
         {
            log.warn("Could not unregister " + sId + " before shutdown.  " +
                     "Root cause is " + e.getMessage());
         }
      }
   }

   private void unregisterDetection(String sId) throws NamingException
   {
      if(log.isTraceEnabled())
      {
         log.trace("unregistering detector " + sId);
      }
      context.unbind(sId);
   }
}
