/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2001
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */
package org.ietf.ldap;

import java.util.*;
import java.io.*;
import java.net.*;

/**
 * Makes a connection to a server from a list using "smart" failover.
 * Connection attempts can be made serially from the same thread, or
 * in parallel by creating a separate thread after the specified delay.
 * Connection setup status is preserved for later attempts, so that servers
 * that are more likely to be available will be tried first.
 * <P>
 * The total time spent opening a connection can be limited with the
 * <CODE>ConnectTimeout</CODE> property.
 * <P>
 * When a connection is successfully created, a socket is opened. The socket 
 * is passed to the LDAPConnThread. The LDAPConnThread must call
 * invalidateConnection() if the connection is lost due to a network or 
 * server error, or disconnect() if the connection is deliberately terminated
 * by the user.
 */
class LDAPConnSetupMgr implements Cloneable, Serializable {

    static final long serialVersionUID = 1519402748245755306L;
    /**
     * Policy for opening a connection when multiple servers are used
     */
    private static final int SERIAL = 0;
    private static final int PARALLEL   = 1;
    
    /**
     * ServerEntry.connSetupStatus possible value. The values also represent
     * the likelihood that the connection will be setup to a server. Lower
     * values have higher priority. See sortDsList() method
     */
    private static final int CONNECTED    = 0;  
    private static final int DISCONNECTED = 1;
    private static final int NEVER_USED   = 2;
    private static final int INTERRUPTED  = 3;
    private static final int FAILED       = 4;
    
    /**
     * Representation for a server in the server list.
     */
    class ServerEntry {
        String host;
        int    port;
        int    connSetupStatus;
        Thread connSetupThread;

        ServerEntry(String host, int port, int status) {
            this.host = host;
            this.port = port;
            connSetupStatus = status;
            connSetupThread = null;
        }
    
        public String toString() {
            return "{" +host+":"+port + " status="+connSetupStatus+"}";
        }
    }

    /**
     * Socket to the connected server
     */
    private Socket _socket = null;
    
    /**
     * Last exception occured during connection setup
     */
    private LDAPException _connException = null;

    /**
     * List of server to use for the connection setup
     */
    ServerEntry[] _dsList;
    
    /**
     * Index of the last connected server
     */    
    private int _dsIdx = -1;

    /**
     * Socket factory for SSL connections
     */
    LDAPSocketFactory _factory;
    
    /**
     * Connection setup policy (PARALLEL or SERIAL)
     */
    int _policy = SERIAL;
    
    /**
     * Delay in ms before another connection setup thread is started.    
     */
    int _connSetupDelay = -1;

    /**
     * The maximum time to wait to established the connection
     */
    int _connectTimeout = 0;

    /**
     *  During connection setup, the current count of servers to which
     *  connection attmpt has been made 
     */
    private transient int _attemptCnt = 0;

    /**
     * Connection IDs for ldap trace messages
     */
    private static int _nextId;
    private int _id;
    
    /**
     * Constructor
     * @param host list of host names to which to connect
     * @param port list of port numbers corresponding to the host list
     * @param factory socket factory for SSL connections     
     */
    LDAPConnSetupMgr(String[] hosts, int[] ports, LDAPSocketFactory factory) {
        _dsList = new ServerEntry[hosts.length];
        for (int i=0; i < hosts.length; i++) {
            _dsList[i] = new ServerEntry(hosts[i], ports[i], NEVER_USED);
        }
        _factory = factory;
        _id = _nextId++;
    }

    /**
     * Try to open the connection to any of the servers in the list, limiting
     * the time waiting for the connection to be established
     * @return connection socket
    */
    synchronized Socket openConnection() throws LDAPException{
        
        long tcur=0, tmax = Long.MAX_VALUE;
        Thread th = null; 
        
        reset();
        
       // If reconnecting, sort dsList so that servers more likly to
        // be available are tried first
        sortDsList();
    
        if (_connectTimeout == 0) {
            // No need for a separate thread, connect time not limited
            connect();
        }
        else {
        
            // Wait for connection at most _connectTimeout milliseconds
            // Run connection setup in a separate thread to monitor the time
            tmax = System.currentTimeMillis() + _connectTimeout;
            th = new Thread (new Runnable() {
                public void run() {
                    connect();
                }
            }, "ConnSetupMgr");
            th.setDaemon(true);
            th.start();
        
            while  (_socket==null && (_attemptCnt < _dsList.length) &&
                   (tcur = System.currentTimeMillis()) < tmax) {
                try {
                    wait(tmax - tcur);
                }
                catch (InterruptedException e) {
                    th.interrupt();
                    cleanup();
                    throw new LDAPInterruptedException("Interrupted connect operation");
                }
            }
        }

        if (_socket != null) {
            return _socket;
        }

        if  ( th != null && (tcur = System.currentTimeMillis()) >= tmax) {
            // We have timed out 
            th.interrupt();
            cleanup();
            throw new LDAPException(
                "connect timeout, " + getServerList() + " might be unreachable",
                LDAPException.CONNECT_ERROR);
        }

        if (_connException != null && _dsList.length == 1) {
            throw _connException;
        }

        throw new LDAPException(
            "failed to connect to server " + getServerList(),
            LDAPException.CONNECT_ERROR);
    }

    private void reset() {
        _socket = null;
        _connException = null;
        _attemptCnt = 0;
                
        for (int i=0; i < _dsList.length; i++) {
            _dsList[i].connSetupThread = null;
        }        
    }
    
    private String getServerList() {
        StringBuffer sb = new StringBuffer();
        for (int i=0; i < _dsList.length; i++) {
            sb.append(i==0 ? "" : " ");
            sb.append(_dsList[i].host);
            sb.append(":");
            sb.append(_dsList[i].port);
        }
        return sb.toString();
   }

    private void connect() {
    
        if (_policy == SERIAL || _dsList.length == 1) {
            openSerial();
        }
        else {
            openParallel();
        }    
    }

    /**
     * Called when the current connection is lost.
     * Put the connected server at the end of the server list for
     * the next connect attempt.    
     */
    synchronized void invalidateConnection() {
        if (_socket != null) {
            _dsList[_dsIdx].connSetupStatus = FAILED;
        
            // Move the entry to the end of the list
            int srvCnt = _dsList.length, j=0;        
            ServerEntry[] newDsList = new ServerEntry[_dsList.length];
            for (int i=0; i < srvCnt; i++) {
                if (i != _dsIdx) {
                    newDsList[j++] = _dsList[i];
                }
            }
            newDsList[j] = _dsList[_dsIdx];
            _dsList = newDsList;
            _dsIdx = j;
        }
    
        _socket = null;
    }
    
    /**
     * Called when the current connection is terminated by the user.
     * Mark the connected server status as DISCONNECTED. This will
     * put it at top of the server list for the next connect attempt.
     */
    void disconnect() {
        if (_socket != null) {
            _dsList[_dsIdx].connSetupStatus =  DISCONNECTED;
        }
    
        _socket = null;
    }

    Socket getSocket() {
        return _socket;
    }
    
    String getHost() {
        if (_dsIdx >= 0) {
            return _dsList[_dsIdx].host;
        }
        return _dsList[0].host;
    }
    
    int getPort() {
        if (_dsIdx >= 0) {
            return _dsList[_dsIdx].port;
        }
        return _dsList[0].port;
    }

    int  getConnSetupDelay() {
        return _connSetupDelay/1000;
    }
    
    /**
     * Selects the connection failover policy
     * @param delay in seconds for the parallel connection setup policy.
     * Possible values are: <br>(delay=-1) use serial policy,<br>
     * (delay=0) start immediately concurrent threads to each specified server
     * <br>(delay>0) create a new connection setup thread after delay seconds
     */
    void setConnSetupDelay(int delay) {
        _policy = (delay < 0) ? SERIAL : PARALLEL;        
        _connSetupDelay = delay*1000;
        
    }
    
    int getConnectTimeout() {
        return _connectTimeout/1000;
    }

    /**
     * Sets the maximum time to spend in the openConnection() call
     * @param timeout in seconds to wait for the connection to be established
     */
    void setConnectTimeout(int timeout) {
        _connectTimeout = timeout*1000;
    }
    
    /**
     * Check if the user has voluntarily closed the connection
     */
    boolean isUserDisconnected() {
        return (_dsIdx >=0 &&
                _dsList[_dsIdx].connSetupStatus == DISCONNECTED);
    }
    
    /**
     * Try  sequentially to open a new connection to a server. 
     */
    private void openSerial() {
        for (int i=0; i < _dsList.length; i++) {
            _dsList[i].connSetupThread = Thread.currentThread();
            connectServer(i);
            if (_socket != null) {
                return;
            }    
        }        
    }
    
    /**
     * Try concurrently to open a new connection a server. Create a separate
     * thread for each connection attempt.
     */
    private synchronized void openParallel() {
        for (int i=0; _socket==null && i < _dsList.length; i++) {
        
            //Create a Thread to execute connectSetver()
            final int dsIdx = i;
            String threadName = "ConnSetupMgr " +
                _dsList[dsIdx].host + ":" + _dsList[dsIdx].port;
            Thread t = new Thread(new Runnable() {
                public void run() {
                    connectServer(dsIdx);
                }
            }, threadName);

            _dsList[dsIdx].connSetupThread = t;
            t.setDaemon(true);
            t.start();
        
            // Wait before starting another thread if the delay is not zero
            if (_connSetupDelay != 0 && i < (_dsList.length-1)) {
                try {
                    wait(_connSetupDelay);
                }
                catch (InterruptedException e) {
                    return;
                }
            }
        }    

        // At this point all threads are started. Wait until first thread
        // succeeds to connect or all threads terminate
    
        while (_socket == null && (_attemptCnt < _dsList.length)) {
        
            // Wait for a thread to terminate
            try {
                wait();
            }
            catch (InterruptedException e) {}
        }
    }

    /**
     * Connect to the server at the given index
     */    
    void connectServer(int idx) {
        ServerEntry entry = _dsList[idx];
        Thread currThread = Thread.currentThread();
        Socket sock = null;
        LDAPException conex = null;
    
        try {
            /* If we are to create a socket ourselves, make sure it has
               sufficient privileges to connect to the desired host */
            if (_factory == null) {
                sock = new Socket (entry.host, entry.port);
                //s.setSoLinger( false, -1 );
            } else {
                sock = _factory.createSocket(entry.host, entry.port);
            }
        }
        catch (IOException e) {    
            conex = new LDAPException("failed to connect to server " 
            + entry.host+":"+entry.port, LDAPException.CONNECT_ERROR);
        }
        catch (LDAPException e) {    
            conex = e;
        }
    
        if (currThread.isInterrupted()) {
            return;
        }
        
        synchronized (this) {
            if (_socket == null && entry.connSetupThread == currThread) {
                entry.connSetupThread = null;
                if (sock != null) {
                    entry.connSetupStatus = CONNECTED;
                    _socket = sock;
                    _dsIdx = idx;
                    cleanup(); // Signal other concurrent threads to terminate
                }
                else {
                    entry.connSetupStatus = FAILED;
                    _connException = conex;
                }
                _attemptCnt++;
                notifyAll();
            }    
        }
    }    

    /**
     * Terminate all concurrently running connection setup threads
     */
    private synchronized void cleanup() {
        Thread currThread = Thread.currentThread();
        for (int i=0; i < _dsList.length; i++) {
            ServerEntry entry = _dsList[i];
            if (entry.connSetupThread != null && entry.connSetupThread != currThread) {
            
                entry.connSetupStatus = INTERRUPTED;
                //Thread.stop() is considered to be dangerous, use Thread.interrupt().
                //interrupt() will however not work if the thread is blocked in the
                //socket library native connect() call, but the connect() will
                //eventually timeout and the thread will die.
                entry.connSetupThread.interrupt();
                
                entry.connSetupThread = null;                
            }
        }
    }    


    /**
     * Sorts Server List so that servers which are more likely to be available
     * are tried first. The likelihood of making a successful connection
     * is determined by the connSetupStatus. Lower values have higher
     * likelihood. Thus, the order of server access is (1) disconnected by
     * the user (2) never used (3) interrupted connection attempt
    *  (4) connection setup failed/connection lost
     */
    private void sortDsList() {
        int srvCnt = _dsList.length;
        for (int i=1; i < srvCnt; i++) {
            for (int j=0; j < i; j++) {
                if (_dsList[i].connSetupStatus < _dsList[j].connSetupStatus) {
                    // swap entries
                    ServerEntry entry = _dsList[j];
                    _dsList[j] = _dsList[i];
                    _dsList[i] = entry;
                }
            }
        }
    }    

    /**
     * This is used only by the ldapjdk test libaray to simulate a
     *  server problem and to test fail-over and rebind
     * @return A flag whether the connection was closed
     */
    boolean breakConnection() {
        try {                
            _socket.close();
            return true;
        }
        catch (Exception e) {
            return false;
        }
    }            

    public String toString() {
        String str = "dsIdx="+_dsIdx+ " dsList=";
        for (int i=0; i < _dsList.length; i++) {
            str += _dsList[i]+ " ";
        }
        return str;
    }    

    int getID() {
        return _id;
    }
    
    String getLDAPUrl() {
        return ((_factory == null) ? "ldap" : "ldaps")  +
               "://" + getHost() + ":" + getPort();
    }
    
    public Object clone() {
        try {
            LDAPConnSetupMgr cloneMgr = (LDAPConnSetupMgr) super.clone();
            cloneMgr._dsList = new ServerEntry[_dsList.length];
            for (int i=0; i<_dsList.length; i++) {
                ServerEntry e = _dsList[i];
                cloneMgr._dsList[i] = new ServerEntry(e.host, e.port, e.connSetupStatus);
            }
            return  cloneMgr;
        }
        catch (CloneNotSupportedException ex) {
            return null;
        }
    }    
}
