/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.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.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.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";

    /**
     * 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());

    /**
     * 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;
    }

    /**
     * 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);
        }
    }

    /**
     * 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 enum = context.listBindings(bindName);
            while(enum.hasMore())
            {
                Binding binding = (Binding) enum.next();
                Detection regMsg = (Detection) binding.getObject();
                // No need to detect myself here
                if(isRemoteDetection(regMsg))
                {
                    if(log.isDebugEnabled())
                    {
                        log.debug("Detected id: " + regMsg.getIdentity().getInstanceId() + ", message: " + regMsg);
                    }
                    if(cleanDetect)
                    {
                        if(log.isDebugEnabled())
                        {
                            log.debug("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 = new Detection(id, locators);
        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
    {
        InvokerLocator[] locators = InvokerRegistry.getRegisteredServerLocators();
        Detection msg = new Detection(id, locators);
        String sId = id.getInstanceId();
        registerDetectionMsg(sId, msg);
    }

    private void registerDetectionMsg(String sId, Detection msg) throws NamingException
    {
        try
        {
            context.bind(sId, msg);
            log.info("Added " + sId + " to registry.");
        }
        catch(NameAlreadyBoundException nabex)
        {
            if(log.isDebugEnabled())
            {
                log.debug(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.
     */
    //TODO: How much info required in order to create local JNDI Server (just host checked now)? -TME
    private void verifyJNDIServer()
    {
        if(host == null || host.length() == 0)
        {
            log.info("JNDI Server configuration information not present so will create a local server.");
            port = 1088;
            host = "localhost";

            try
            {
                //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)
            {
                e.printStackTrace();
            }
        }
    }

    /**
     * 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(DETECTION_SUBCONTEXT_NAME);
        }
        catch(NamingException e)
        {
            context = initialContext.createSubcontext(DETECTION_SUBCONTEXT_NAME);
        }
    }

    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.isDebugEnabled())
        {
            log.debug("unregistering detector " + sId);
        }
        context.unbind(sId);
    }
}