/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */
package netscape.ldap;

import java.util.*;
import netscape.ldap.client.*;
import netscape.ldap.client.opers.*;
import netscape.ldap.ber.stream.*;
import netscape.ldap.util.*;
import java.io.*;
import java.net.*;

/**
 * Represents a connection to an LDAP server. <P>
 *
 * Use objects of this class to perform LDAP operations (such as
 * search, modify, and add) on an LDAP server. <P>
 *
 * To perform an LDAP operation on a server, you need to follow
 * these steps: <P>
 *
 * <OL>
 * <LI>Create a new <CODE>LDAPConnection</CODE> object.
 * <LI>Use the <CODE>connect</CODE> method to connect to the
 * LDAP server.
 * <LI>Use the <CODE>authenticate</CODE> method to authenticate
 * to server.
 * <LI>Perform the LDAP operation.
 * <LI>Use the <CODE>disconnect</CODE> method to disconnect from
 * the server when done.
 * </OL>
 * <P>
 *
 * All operations block until completion (with the exception of
 * the search method when the results are not all returned at
 * the same time).
 * <P>
 *
 * This class also specifies a default set of search constraints
 * (such as the maximum number of results returned in a search)
 * which apply to all operations. To get and set these constraints,
 * use the <CODE>getOption</CODE> and <CODE>setOption</CODE> methods.
 * To override these constraints for an individual search operation,
 * define a new set of constraints by creating a <CODE>LDAPSearchConstraints</CODE>
 * option and pass the object to the <CODE>search</CODE> method.
 * <P>
 *
 * If you set up your client to follow referrals automatically,
 * an operation that results in a referral will create a new connection
 * to the LDAP server identified in the referral.  In order to have
 * your client authenticate to that LDAP server automatically, you need
 * to define a class that implements the <CODE>LDAPRebind</CODE> interface.
 * In your definition of the class, you need to define a
 * <CODE>getRebindAuthentication</CODE> method that creates an <CODE>LDAPRebindAuth</CODE>
 * object containing the distinguished name and password to use for reauthentication.
 * <P>
 *
 * Most errors that occur raise the same exception (<CODE>LDAPException</CODE>).
 * In order to determine the exact problem that occurred, you can retrieve the
 * result code from this exception and compare its value against a set of defined
 * result codes.
 * <P>
 *
 * @version 1.0
 * @see netscape.ldap.LDAPSearchConstraints
 * @see netscape.ldap.LDAPRebind
 * @see netscape.ldap.LDAPRebindAuth
 * @see netscape.ldap.LDAPException
 */
public class LDAPConnection implements LDAPv3, Cloneable {

    /**
     * Version of the LDAP protocol used by default.
     * <CODE>LDAP_VERSION</CODE> is 2, so your client will
     * attempt to authenticate to LDAP servers as an LDAP v2 client.
     * The following is an example of some code that prints the
     * value of this variable:
     * <P>
     *
     * <PRE>
     * LDAPConnection ld = new LDAPConnection();
     * System.out.println( "The default LDAP protocol version used is "
     *                      ld.LDAP_VERSION );
     * </PRE>
     *
     * If you want to authenticate as an LDAP v3 client,
     * use the <A HREF="#authenticate(int, java.lang.String, java.lang.String)"><CODE>authenticate(int version, String dn, String passwd)</CODE></A> method.
     * For example:
     * <P>
     *
     * <PRE>
     * ld.authenticate( 3, myDN, myPW );
     * </PRE>
     *
     * @see netscape.ldap.LDAPConnection#authenticate(int, java.lang.String, java.lang.String)
     */
    public final static int LDAP_VERSION              = 2;
    /**
     * Name of the property specifying the version of the SDK. <P>
     *
     * To get the version number, pass this name to the
     * <CODE>getProperty</CODE> method.  The SDK version number
     * is a <CODE>Float</CODE> type. For example:<P>
     * <PRE>
     *      ...
     *      Float sdkVersion = ( Float )myConn.getProperty( myConn.LDAP_PROPERTY_SDK );
     *      System.out.println( "SDK version: " + sdkVersion );
     *      ... </PRE>
     * @see netscape.ldap.LDAPConnection#getProperty(java.lang.String)
     */
    public final static String LDAP_PROPERTY_SDK      = "version.sdk";
    /**
     * Name of the property specifying the highest supported version of
     * the LDAP protocol. <P>
     *
     * To get the version number, pass this name to the
     * <CODE>getProperty</CODE> method.  The LDAP protocol version number
     * is a <CODE>Float</CODE> type. For example:<P>
     * <PRE>
     *      ...
     *      Float LDAPVersion = ( Float )myConn.getProperty( myConn.LDAP_PROPERTY_PROTOCOL );
     *      System.out.println( "Highest supported LDAP protocol version: " + LDAPVersion );
     *      ... </PRE>
     * @see netscape.ldap.LDAPConnection#getProperty(java.lang.String)
     */
    public final static String LDAP_PROPERTY_PROTOCOL = "version.protocol";
    /**
     * Name of the property specifying the types of authentication allowed by this
     * API (for example, anonymous authentication and simple authentication). <P>
     *
     * To get the supported types, pass this name to the
     * <CODE>getProperty</CODE> method.  The value of this property is
     * a <CODE>String</CODE> type. For example:<P>
     * <PRE>
     *      ...
     *      String authTypes = ( String )myConn.getProperty( myConn.LDAP_PROPERTY_SECURITY );
     *      System.out.println( "Supported authentication types: " + authTypes );
     *      ... </PRE>
     * @see netscape.ldap.LDAPConnection#getProperty(java.lang.String)
     */
    public final static String LDAP_PROPERTY_SECURITY = "version.security";

    /**
     * Constants
     */
    private final static String defaultFilter = "(objectClass=*)";
    private final static LDAPSearchConstraints readConstraints = new
      LDAPSearchConstraints();

    /**
     * Internal variables
     */
    transient private LDAPSearchConstraints defaultConstraints = new
      LDAPSearchConstraints ();
    transient private Vector responseListeners;
    transient private Vector searchListeners;
    transient private boolean bound;
    transient private String host;
    transient private String[] m_hostList;
    transient private int port;
    transient private int[] m_portList;
    transient private int m_defaultPort;
    transient private String prevBoundDN;
    transient private String prevBoundPasswd;
    transient private String boundDN;
    transient private String boundPasswd;
    transient private int protocolVersion = LDAP_VERSION;
    transient private LDAPSocketFactory m_factory;
    /* th does all socket i/o for the object and any clones */
    transient private LDAPConnThread th = null;
    /* To manage received server controls on a per-thread basis,
       we keep a table of active threads and a table of controls,
       indexed by thread */
    transient private Vector m_attachedList = new Vector();
    transient private Hashtable m_responseControlTable = new Hashtable();
    transient private LDAPCache m_cache = null;
    static Hashtable m_threadConnTable = new Hashtable();

    // this handles the case when the client lost the connection with the
    // server. After the client reconnects with the server, the bound resets
    // to false. If the client used to have anonymous bind, then this boolean
    // will take care of the case whether the client should send anonymous bind
    // request to the server.
    private boolean m_anonymousBound = false;

    private Object m_security = null;
    private boolean saslBind = false;
    private Object m_mechanismDriver;
    private Properties m_securityProperties;
    private Object m_clientCB;
    private Hashtable m_methodLookup = new Hashtable();
    private LDAPConnection m_referralConnection;

    /**
     * Properties
     */
    private final static Float SdkVersion = new Float(3.04f);
    private final static Float ProtocolVersion = new Float(3.0f);
    private final static String SecurityVersion = new String("none,simple,sasl");
    private final static Float MajorVersion = new Float(3.0f);
    private final static Float MinorVersion = new Float(0.05f);
    private final static String DELIM = "#";
    private final static String PersistSearchPackageName =
      "netscape.ldap.controls.LDAPPersistSearchControl";

   /**
    * Constructs a new <CODE>LDAPConnection</CODE> object,
    * which represents a connection to an LDAP server. <P>
    *
    * Calling the constructor does not actually establish
    * the connection.  To connect to the LDAP server, use the
    * <CODE>connect</CODE> method.
    *
    * @see netscape.ldap.LDAPConnection#connect(java.lang.String, int)
    * @see netscape.ldap.LDAPConnection#authenticate(java.lang.String, java.lang.String)
    */
    public LDAPConnection () {
        super();
        port = -1;
        m_factory = null;
    }

    /**
     * Constructs a new <CODE>LDAPConnection</CODE> object that
     * will use the specified socket factory class to create
     * socket connections. The socket factory class must implement
     * the <CODE>LDAPSocketFactory</CODE> interface. <BR>
     * (For example, the <CODE>LDAPSSLSocketFactory</CODE>
     * class implements this interface.)
     * <P>
     *
     * Note that calling the <CODE>LDAPConnection</CODE> constructor
     * does not actually establish a connection to an LDAP server.
     * To connect to an LDAP server, use the
     * <CODE>connect</CODE> method.  The socket connection will be
     * constructed when this method is called.
     * <P>
     *
     * @see netscape.ldap.LDAPSocketFactory
     * @see netscape.ldap.LDAPSSLSocketFactory
     * @see netscape.ldap.LDAPConnection#connect(java.lang.String, int)
     * @see netscape.ldap.LDAPConnection#authenticate(java.lang.String, java.lang.String)
     * @see netscape.ldap.LDAPConnection#getSocketFactory
     * @see netscape.ldap.LDAPConnection#setSocketFactory(netscape.ldap.LDAPSocketFactory)
     */
    public LDAPConnection ( LDAPSocketFactory factory ) {
        super();
        port = -1;
        m_factory = factory;
    }

    /**
     * Finalize method, which disconnects from the LDAP server.
     * @exception LDAPException Thrown when the connection cannot be disconnected.
     */
    public void finalize() throws LDAPException
    {
        if (th != null)
            disconnect();
    }

    /**
     *  Sets the specified <CODE>LDAPCache</CODE> object as the
     *  cache for the <CODE>LDAPConnection</CODE> object.
     *  <P>
     *
     *  @param cache The <CODE>LDAPCache</CODE> object representing
     *   the cache you want used by the current connection.
     *  @see netscape.ldap.LDAPCache
     *  @see netscape.ldap.LDAPConnection#getCache
     */
    public void setCache(LDAPCache cache) {
        m_cache = cache;
    }

    /**
     * Gets the <CODE>LDAPCache</CODE> object associated with
     * the current <CODE>LDAPConnection</CODE> object.
     * <P>
     *
     * @return The <CODE>LDAPCache</CODE> object representing
     *   the cache used by the current connection.
     * @see netscape.ldap.LDAPCache
     * @see netscape.ldap.LDAPConnection#setCache(netscape.ldap.LDAPCache)
     */
    public LDAPCache getCache() {
        return m_cache;
    }

    /**
     * Gets a property of a connection. <P>
     *
     * You can get the following properties for a given connection: <P>
     * <UL TYPE="DISC">
     * <LI><CODE>LDAP_PROPERTY_SDK</CODE> <P>
     * To get the version of this SDK, get this property.  The value of
     * this property is a <CODE>Float</CODE> data type. <P>
     * <LI><CODE>LDAP_PROPERTY_PROTOCOL</CODE> <P>
     * To get the highest supported version of the LDAP protocol, get
     * this property.
     * The value of this property is a <CODE>Float</CODE> data type. <P>
     * <LI><CODE>LDAP_PROPERTY_SECURITY</CODE> <P>
     * To get a comma-separated list of the types of authentication
     * supported, get this property.  The value of this property is a
     * <CODE>String</CODE>. <P>
     * </UL>
     * <P>
     *
     * For example, the following section of code gets the version of
     * the SDK.<P>
     *
     * <PRE>
     *       ...
     *       Float sdkVersion = ( Float )myConn.getProperty( myConn.LDAP_PROPERTY_SDK );
     *       System.out.println( "SDK version: " + sdkVersion );
     *       ... </PRE>
     *
     * @param name Name of the property (for example,
     * <CODE>LDAP_PROPERTY_SDK</CODE>). <P>
     *
     * @return The value of the property. <P>
     *
     * Since the return value is an object, you
     * should recast it as the appropriate type.
     * (For example, when getting the <CODE>LDAP_PROPERTY_SDK</CODE> property,
     * recast the return value as a <CODE>Float</CODE>.) <P>
     *
     * If you pass this method an unknown property name, the method
     * returns null. <P>
     *
     * @exception LDAPException Unable to get the value of the
     * specified property. <P>
     *
     * @see netscape.ldap.LDAPConnection#LDAP_PROPERTY_SDK
     * @see netscape.ldap.LDAPConnection#LDAP_PROPERTY_PROTOCOL
     * @see netscape.ldap.LDAPConnection#LDAP_PROPERTY_SECURITY
     */
    public Object getProperty(String name) throws LDAPException {
        if (name.equals(LDAP_PROPERTY_SDK))
            return SdkVersion;
        else if (name.equals(LDAP_PROPERTY_PROTOCOL))
            return ProtocolVersion;
        else if (name.equals(LDAP_PROPERTY_SECURITY))
            return SecurityVersion;
        else if (name.equals("version.major"))
            return MajorVersion;
        else if (name.equals("version.minor"))
            return MinorVersion;
        else
            return null;
    }

    /**
     * This method is reserved for future use and does not currently
     * allow you to set any properties.
     * <P>
     *
     * @param name Name of the property that you want to set.
     * @param val Value that you want to set.
     * @exception LDAPException Unable to set the value of the specified
     * property.
     */
    public void setProperty(String name, Object val) throws LDAPException {
        throw new LDAPException("No property has been set");
    }

    /**
     * Sets the LDAP protocol version that your client prefers to use when
     * connecting to the LDAP server.
     * <P>
     *
     * @param version The LDAP protocol version that your client uses.
     */
    private void setProtocolVersion(int version) {
        protocolVersion = version;
    }

    /**
     * Returns the host name of the LDAP server to which you are connected.
     * @return Host name of the LDAP server.
     */
    public String getHost () {
        return host;
    }

    /**
     * Returns the port number of the LDAP server to which you are connected.
     * @return Port number of the LDAP server.
     */
    public int getPort () {
        return port;
    }

    /**
     * Returns the distinguished name (DN) used for authentication over
     * this connection.
     * @return Distinguished name used for authentication over this connection.
     */
    public String getAuthenticationDN () {
        return boundDN;
    }

    /**
     * Returns the password used for authentication over this connection.
     * @return Password used for authentication over this connection.
     */
    public String getAuthenticationPassword () {
        return boundPasswd;
    }

    /**
     * Gets the object representing the socket factory used to establish
     * a connection to the LDAP server.
     * <P>
     *
     * @return The object representing the socket factory used to
     *  establish a connection to a server.
     * @see netscape.ldap.LDAPSocketFactory
     * @see netscape.ldap.LDAPSSLSocketFactory
     * @see netscape.ldap.LDAPConnection#setSocketFactory(netscape.ldap.LDAPSocketFactory)
     */
    public LDAPSocketFactory getSocketFactory () {
        return m_factory;
    }

    /**
     * Specifies the object representing the socket factory that you
     * want to use to establish a connection to a server.
     * <P>
     *
     * @param factory The object representing the socket factory that
     *  you want to use to establish a connection to a server.
     * @see netscape.ldap.LDAPSocketFactory
     * @see netscape.ldap.LDAPSSLSocketFactory
     * @see netscape.ldap.LDAPConnection#getSocketFactory
     */
    public void setSocketFactory (LDAPSocketFactory factory) {
        m_factory = factory;
    }

    /**
     * Indicates whether the connection represented by this object
     * is open at this time.
     * @return If connected to an LDAP server over this connection,
     * returns <CODE>true</CODE>.  If not connected to an LDAP server,
     * returns <CODE>false</CODE>.
     */
    public boolean isConnected() {
        // This is the hack: If the user program calls isConnected() when
        // the thread is about to shut down, the isConnected might get called
        // before the deregisterConnection(). We add the yield() so that 
        // the deregisterConnection() will get called first. 
        // This problem only exists on Solaris.
        Thread.yield();
        return (th != null);
    }

    /**
     * Indicates whether this client has authenticated to the LDAP server
     * @return If authenticated, returns <CODE>true</CODE>. If not
     * authenticated, or if authenticated as an anonymous user (with
     * either a blank name or password), returns <CODE>false</CODE>.
     */
    public boolean isAuthenticated () {
        if (bound) {
            if ((boundDN == null) || boundDN.equals("") ||
              (boundPasswd == null) || boundPasswd.equals(""))
                return false;
        }
        return bound;
    }

    /**
     * Connects to the specified host and port.  If this LDAPConnection object
     * represents an open connection, the connection is closed first
     * before the new connection is opened.
     * <P>
     *
     * For example, the following section of code establishes a connection with
     * the LDAP server running on the host ldap.netscape.com and the port 389.
     * <P>
     *
     * <PRE>
     * String ldapHost = "ldap.netscape.com";
     * int ldapPort = 389;
     * LDAPConnection myConn = new LDAPConnection();
     * try {
     *     myConn.connect( ldapHost, ldapPort );
     * } catch ( LDAPException e ) {
     *     System.out.println( "Unable to connect to " + ldapHost +
     *                         " at port " + ldapPort );
     *     return;
     * }
     * System.out.println( "Connected to " + ldapHost + " at port " + ldapPort )
     * </PRE>
     *
     * @param host Host name of the LDAP server that you want to connect to.
     * This value can also be a space-delimited list of hostnames or
     * hostnames and port numbers (using the syntax
     * <I>hostname:portnumber</I>). Your client application or applet
     * will attempt to contact each host in the order you specify until a
     * connection is established. For example, you can specify the following
     * values for the <CODE>host</CODE> argument:<BR>
     *<PRE>
     *   myhost
     *   myhost hishost:389 herhost:5000 whathost
     *   myhost:686 myhost:389 hishost:5000 whathost:1024
     *</PRE>
     * @param port Port number of the LDAP server that you want to connect to.
     * This parameter is ignored for any host in the <CODE>host</CODE>
     * parameter which includes a colon and port number.
     * @exception LDAPException The connection failed.
     */
    public void connect(String host, int port) throws LDAPException {
      connect( host, port, null, null, defaultConstraints, false );
    }

    /**
     * Connects to the specified host and port and uses the specified DN and
     * password to authenticate to the server.  If this LDAPConnection object
     * represents an open connection, the connection is closed first
     * before the new connection is opened.
     * <P>
     *
     * For example, the following section of code establishes a connection
     * with the LDAP server running on ldap.netscape.com at port 389.  The
     * example also attempts to authenticate the client as Barbara Jensen.
     * <P>
     *
     * <PRE>
     * String ldapHost = "ldap.netscape.com";
     * int ldapPort = 389;
     * String myDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * String myPW = "hifalutin";
     * LDAPConnection myConn = new LDAPConnection();
     * try {
     *     myConn.connect( ldapHost, ldapPort, myDN, myPW );
     * } catch ( LDAPException e ) {
     *     switch( e.getLDAPResultCode() ) {
     *         case e.NO_SUCH_OBJECT:
     *             System.out.println( "The specified user does not exist." );
     *             break;
     *         case e.INVALID_CREDENTIALS:
     *             System.out.println( "Invalid password." );
     *             break;
     *         default:
     *             System.out.println( "Error number: " + e.getLDAPResultCode() );
     *             System.out.println( "Failed to connect to " + ldapHost + " at port " + ldapPort );
     *             break;
     *     }
     *     return;
     * }
     * System.out.println( "Connected to " + ldapHost + " at port " + ldapPort );
     * </PRE>
     *
     * @param host Host name of the LDAP server that you want to connect to.
     * This value can also be a space-delimited list of hostnames or
     * hostnames and port numbers (using the syntax
     * <I>hostname:portnumber</I>). Your client application or applet
     * will attempt to contact each host in the order you specify until a
     * connection is established. For example, you can specify the following
     * values for the <CODE>host</CODE> argument:<BR>
     *<PRE>
     *   myhost
     *   myhost hishost:389 herhost:5000 whathost
     *   myhost:686 myhost:389 hishost:5000 whathost:1024
     *</PRE>
     * @param port Port number of the LDAP server that you want to connect to.
     * This parameter is ignored for any host in the <CODE>host</CODE>
     * parameter which includes a colon and port number.
     * @param dn Distinguished name used for authentication
     * @param passwd Password used for authentication
     * @exception LDAPException The connection or authentication failed.
     */
    public void connect(String host, int port, String dn, String passwd)
        throws LDAPException {
        connect(host, port, dn, passwd, defaultConstraints, true);
    }

    /**
     * Connects to the specified host and port and uses the specified DN and
     * password to authenticate to the server.  If this LDAPConnection object
     * represents an open connection, the connection is closed first
     * before the new connection is opened. This method allows the user to 
     * specify the preferences for the bind operation.
     * 
     * @param host Host name of the LDAP server that you want to connect to.
     * This value can also be a space-delimited list of hostnames or
     * hostnames and port numbers (using the syntax
     * <I>hostname:portnumber</I>). Your client application or applet
     * will attempt to contact each host in the order you specify until a
     * connection is established. For example, you can specify the following
     * values for the <CODE>host</CODE> argument:<BR>
     *<PRE>
     *   myhost
     *   myhost hishost:389 herhost:5000 whathost
     *   myhost:686 myhost:389 hishost:5000 whathost:1024
     *</PRE>
     * @param port Port number of the LDAP server that you want to connect to.
     * This parameter is ignored for any host in the <CODE>host</CODE>
     * parameter which includes a colon and port number.
     * @param dn Distinguished name used for authentication
     * @param passwd Password used for authentication
     * @param cons Preferences for the bind operation.
     * @exception LDAPException The connection or authentication failed.
     */
    public void connect(String host, int port, String dn, String passwd,
        LDAPSearchConstraints cons) throws LDAPException {
        connect(host, port, dn, passwd, cons, true);
    }

    private void connect(String host, int port, String dn, String passwd,
      LDAPSearchConstraints cons, boolean doAuthenticate) 
        throws LDAPException {
        if (th != null) disconnect ();
        if ((host == null) || (host.equals("")))
            throw new LDAPException ( "no host for connection",
                                  LDAPException.PARAM_ERROR );

        /* Parse the list of hosts */
        m_defaultPort = port;
        StringTokenizer st = new StringTokenizer( host );
        m_hostList = new String[st.countTokens()];
        m_portList = new int[st.countTokens()];
        int i = 0;
        while( st.hasMoreTokens() ) {
            String s = st.nextToken();
            int colon = s.indexOf( ':' );
            if ( colon > 0 ) {
                m_hostList[i] = s.substring( 0, colon );
                m_portList[i] = Integer.parseInt( s.substring( colon+1 ) );
            } else {
                m_hostList[i] = s;
                m_portList[i] = m_defaultPort;
            }
            i++;
        }

        /* Try each possible host until a connection attempt doesn't cause
           an exception */
        LDAPException conex = null;
        for( i = 0; i < m_hostList.length; i++ ) {
            try {
                this.host = m_hostList[i];
                this.port = m_portList[i];
                connect ();
                conex = null;
                break;
            } catch ( LDAPException e ) {
                conex = e;
            }
        }
        if ( conex != null ) {
            /* All connection attempts failed */
            this.host = m_hostList[0];
            this.port = m_defaultPort;
            throw conex;
        }

        if (doAuthenticate)
            authenticate(dn, passwd, cons);
    }

    /**
     * Connects to the specified host and port and uses the specified DN and
     * password to authenticate to the server, with the specified LDAP
     * protocol version. If the server does not support the requested
     * protocol version, an exception is thrown. If this LDAPConnection
     * object represents an open connection, the connection is closed first
     * before the new connection is opened. This is equivalent to
     * <CODE>connect(host, port)</CODE> followed by <CODE>authenticate(version, dn, passwd)</CODE>.<P>
     *
     * @param version LDAP protocol version requested: currently 2 or 3
     * @param host Contains a hostname or dotted string representing
     *   the IP address of a host running an LDAP server to connect to.
     *   Alternatively, it may contain a list of host names, space-delimited.
     *   Each host name may include a trailing colon and port number. In the
     *   case where more than one host name is specified, each host name in
     *   turn will be contacted until a connection can be established.<P>
     *
     * <PRE>
     *   Examples:
     *      "directory.knowledge.com"
     *      "199.254.1.2"
     *      "directory.knowledge.com:1050 people.catalog.com 199.254.1.2"
     * </PRE>
     * <P>
     * @param port Contains the TCP or UDP port number to connect to or contact.
     *   The default LDAP port is 389. "port" is ignored for any host name which
     *   includes a colon and port number.
     * @param dn If non-null and non-empty, specifies that the connection and
     *   all operations through it should be authenticated with dn as the
     *   distinguished name.
     * @param passwd If non-null and non-empty, specifies that the connection and
     *   all operations through it should be authenticated with dn as the
     *   distinguished name and passwd as password.
     * @exception LDAPException The connection or authentication failed.
     */
    public void connect(int version, String host, int port, String dn,
        String passwd) throws LDAPException {

        connect(version, host, port, dn, passwd, defaultConstraints);
    }

    /**
     * Connects to the specified host and port and uses the specified DN and
     * password to authenticate to the server, with the specified LDAP
     * protocol version. If the server does not support the requested
     * protocol version, an exception is thrown. This method allows the user
     * to specify preferences for the bind operation. If this LDAPConnection
     * object represents an open connection, the connection is closed first
     * before the new connection is opened. This is equivalent to
     * <CODE>connect(host, port)</CODE> followed by <CODE>authenticate(version, dn, passwd)</CODE>.<P>
     *
     * @param version LDAP protocol version requested: currently 2 or 3
     * @param host Contains a hostname or dotted string representing
     *   the IP address of a host running an LDAP server to connect to.
     *   Alternatively, it may contain a list of host names, space-delimited.
     *   Each host name may include a trailing colon and port number. In the
     *   case where more than one host name is specified, each host name in
     *   turn will be contacted until a connection can be established.<P>
     *
     * <PRE>
     *   Examples:
     *      "directory.knowledge.com"
     *      "199.254.1.2"
     *      "directory.knowledge.com:1050 people.catalog.com 199.254.1.2"
     * </PRE>
     * <P>
     * @param port Contains the TCP or UDP port number to connect to or contact.
     *   The default LDAP port is 389. "port" is ignored for any host name which
     *   includes a colon and port number.
     * @param dn If non-null and non-empty, specifies that the connection and
     *   all operations through it should be authenticated with dn as the
     *   distinguished name.
     * @param passwd If non-null and non-empty, specifies that the connection and
     *   all operations through it should be authenticated with dn as the
     *   distinguished name and passwd as password.
     * @param cons Preferences for the bind operation.
     * @exception LDAPException The connection or authentication failed.
     */
    public void connect(int version, String host, int port, String dn,
        String passwd, LDAPSearchConstraints cons) throws LDAPException {

        setProtocolVersion(version);
        connect(host, port, dn, passwd, cons);
    }

    /**
     * Internal routine to connect with internal params
     * @exception LDAPException failed to connect
     */
    private synchronized void connect () throws LDAPException {
        if (th != null) return;

        if (host == null || port < 0)
            throw new LDAPException ( "no connection parameters",
                                 LDAPException.PARAM_ERROR );

        th = getNewThread(host, port, m_factory, m_cache);
    }

    private synchronized LDAPConnThread getNewThread(String host, int port,
      LDAPSocketFactory factory, LDAPCache cache) throws LDAPException {

        LDAPConnThread newThread = null;
        Vector v = null;

        synchronized(m_threadConnTable) {

            Enumeration keys = m_threadConnTable.keys();
            boolean connExist = false;

            // transverse each thread
            while (keys.hasMoreElements()) {
                LDAPConnThread connThread = (LDAPConnThread)keys.nextElement();
                Vector connVector = (Vector)m_threadConnTable.get(connThread);
                Enumeration enumv = connVector.elements();

                // transverse through each LDAPConnection under the same thread
                while (enumv.hasMoreElements()) {
                    LDAPConnection conn = (LDAPConnection)enumv.nextElement();

                    // this is not the brand new connection
                    if (conn.equals(this)) {
                        connExist = true;

                        if (!connThread.isAlive()) {
                            // need to move all the LDAPConnections under the dead thread
                            // to the new thread
                            try {
                                newThread = new LDAPConnThread(host, port, factory, cache);
                                newThread.setMaxBacklog(
                                    getSearchConstraints().getMaxBacklog() );
                                v = (Vector)m_threadConnTable.remove(connThread);
                                break;
                            } catch (Exception e) {
                                throw new LDAPException ("unable to establish connection",
                                    LDAPException.UNAVAILABLE);
                            }
                        }
                        break;
                    }
                }

                if (connExist) break;
            }

            // if this connection is new or the corresponding thread for the current
            // connection is dead
            if (!connExist) {
                try {
                    newThread = new LDAPConnThread(host, port, factory, cache);
                    newThread.setMaxBacklog( getSearchConstraints().getMaxBacklog() );
                    v = new Vector();
                    v.addElement(this);
                } catch (Exception e) {
                    throw new LDAPException ("unable to establish connection",
                        LDAPException.UNAVAILABLE);
                }
            }

            // add new thread to the table
            if (newThread != null) {
                m_threadConnTable.put(newThread, v);
                for (int i=0, n=v.size(); i<n; i++) {
                    LDAPConnection c = (LDAPConnection)v.elementAt(i);
                    newThread.register(c);
                    c.th = newThread;
                }
            }
        }
        return newThread;
    }

    /**
     * Abandons a current search operation, notifying the server not
     * to send additional search results.
     *
     * @param searchResults The search results returned when the search
     * was started.
     * @exception LDAPException Failed to notify the LDAP server.
     * @see netscape.ldap.LDAPConnection#search(java.lang.String, int, java.lang.String, java.lang.String[], boolean, netscape.ldap.LDAPSearchConstraints)
     * @see netscape.ldap.LDAPSearchResults
     */
    public void abandon( LDAPSearchResults searchResults )
                             throws LDAPException {
        if ( (th == null) || (searchResults == null) )
            return;

        searchResults.abandon();
        int id = searchResults.getID();
        for (int i=0; i<3; i++) {
            try {
                /* Tell listener thread to discard results */
                th.abandon( id );

                /* Tell server to stop sending results */
                th.sendRequest(new JDAPAbandonRequest(id), null, defaultConstraints);

                /* No response is forthcoming */
                break;
            } catch (NullPointerException ne) {
                // do nothing
            }
        }
        if (th == null)
            throw new LDAPException("Failed to send abandon request to the server.",
              LDAPException.OTHER);
    }

    /**
     * Authenticates to the LDAP server (that you are currently
     * connected to) using the specified name and password.
     * If you are not already connected to the LDAP server, this
     * method attempts to reconnect to the server.
     * <P>
     *
     * For example, the following section of code authenticates the
     * client as Barbara Jensen.  The code assumes that the client
     * has already established a connection with an LDAP server.
     * <P>
     *
     * <PRE>
     * String myDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * String myPW = "hifalutin";
     * try {
     *     myConn.authenticate( myDN, myPW );
     * } catch ( LDAPException e ) {
     *     switch( e.getLDAPResultCode() ) {
     *         case e.NO_SUCH_OBJECT:
     *             System.out.println( "The specified user does not exist." );
     *             break;
     *         case e.INVALID_CREDENTIALS:
     *             System.out.println( "Invalid password." );
     *             break;
     *         default:
     *             System.out.println( "Error number: " + e.getLDAPResultCode() );
     *             System.out.println( "Failed to authentice as " + myDN );
     *             break;
     *     }
     *     return;
     * }
     * System.out.println( "Authenticated as " + myDN );
     * </PRE>
     *
     * @param dn Distinguished name used for authentication.
     * @param passwd Password used for authentication.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(String dn, String passwd) throws LDAPException {
        authenticate(protocolVersion, dn, passwd, defaultConstraints);
    }

    /**
     * Authenticates to the LDAP server (that you are currently
     * connected to) using the specified name and password. The
     * default protocol version (version 2) is used. If the server
     * doesn't support the default version, an LDAPException is thrown
     * with the error code PROTOCOL_ERROR. This method allows the
     * user to specify the preferences for the bind operation.
     *
     * @param dn Distinguished name used for authentication.
     * @param passwd Password used for authentication.
     * @param cons Preferences for the bind operation.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(String dn, String passwd,
      LDAPSearchConstraints cons) throws LDAPException {
        authenticate(protocolVersion, dn, passwd, cons);
    }

    /**
     * Authenticates to the LDAP server (that you are currently
     * connected to) using the specified name and password, and
     * requesting that the server use at least the specified
     * protocol version. If the server doesn't support that
     * level, an LDAPException is thrown with the error code
     * PROTOCOL_ERROR.
     *
     * @param version Required LDAP protocol version.
     * @param dn Distinguished name used for authentication.
     * @param passwd Password used for authentication.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(int version, String dn, String passwd)
      throws LDAPException {
        authenticate(version, dn, passwd, defaultConstraints);
    }

    /**
     * Authenticates to the LDAP server (that you are currently
     * connected to) using the specified name and password, and
     * requesting that the server use at least the specified
     * protocol version. If the server doesn't support that
     * level, an LDAPException is thrown with the error code
     * PROTOCOL_ERROR. This method allows the user to specify the
     * preferences for the bind operation.
     *
     * @param version Required LDAP protocol version.
     * @param dn Distinguished name used for authentication.
     * @param passwd Password used for authentication.
     * @param cons Preferences for the bind operation.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(int version, String dn, String passwd,
      LDAPSearchConstraints cons) throws LDAPException {
        prevBoundDN = boundDN;
        prevBoundPasswd = boundPasswd;
        boundDN = dn;
        boundPasswd = passwd;

        if ((prevBoundDN == null) || (prevBoundPasswd == null))
            m_anonymousBound = true;
        else
            m_anonymousBound = false;

        bind (version, true, cons);
    }

    /**
     * Authenticates to the LDAP server (that the object is currently
     * connected to) using the specified name and a specified SASL mechanism
     * or set of mechanisms. If the requested SASL mechanism is not
     * available, an exception is thrown.  If the object has been
     * disconnected from an LDAP server, this method attempts to reconnect
     * to the server. If the object had already authenticated, the old
     * authentication is discarded.
     *
     * @param dn If non-null and non-empty, specifies that the connection and
     * all operations through it should be authenticated with dn as the
     * distinguished name.
     * @param mechanism A single mechanism name, e.g. "GSSAPI".
     * @param packageName A package from which to instantiate the Mechanism
     * Driver, e.g. "myclasses.SASL.mechanisms". If null, a system default
     * is used.
     * @param getter A class which may be called by the Mechanism Driver to
     * obtain additional information required.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(String dn, String mechanism, String packageName,
        Properties props, Object getter) throws LDAPException {

        try {
            Object[] args = new Object[2];
            args[0] = mechanism;
            args[1] = packageName;
            String[] argNames = new String[2];
            argNames[0] = "java.lang.String";
            argNames[1] = "java.lang.String";

            // Get a mechanism driver
            m_mechanismDriver = invokeMethod(null,
              "com.netscape.sasl.SASLMechanismFactory", "getMechanismDriver",
              args, argNames);

        } catch (Exception e) {
            throw new LDAPException(e.toString(), LDAPException.OTHER);
        }

        m_securityProperties = props;
        m_clientCB = getter;
        boundDN = dn;
        saslBind(true);
    }

    /**
     * Authenticates to the LDAP server (that the object is currently
     * connected to) using the specified name and a specified SASL mechanism
     * or set of mechanisms. If the requested SASL mechanism is not
     * available, an exception is thrown.  If the object has been
     * disconnected from an LDAP server, this method attempts to reconnect
     * to the server. If the object had already authenticated, the old
     * authentication is discarded.
     *
     * @param dn If non-null and non-empty, specifies that the connection and
     * all operations through it should be authenticated with dn as the
     * distinguished name.
     * @param mechanisms A list of acceptable mechanisms. The first one
     * for which a Mechanism Driver can be instantiated is returned.
     * @param packageName A package from which to instantiate the Mechanism
     * Driver, e.g. "myclasses.SASL.mechanisms". If null, a system default
     * is used.
     * @param getter A class which may be called by the Mechanism Driver to
     * obtain additional information required.
     * @exception LDAPException Failed to authenticate to the LDAP server.
     */
    public void authenticate(String dn, String[] mechanisms, String packageName,
        Properties props, Object getter) throws LDAPException {

        for (int i=0; i<mechanisms.length; i++) {
            try {
                authenticate(dn, mechanisms[i], packageName, props, getter);
                if (m_mechanismDriver != null)
                    return;
            } catch (LDAPException e) {
            }
        }

        throw new LDAPException("Failed to get mechanism driver",
          LDAPException.OTHER);
    }

    private void saslBind(boolean rebind) throws LDAPException {

        saslBind = true;
        protocolVersion = 3;

        if (th == null) {
            bound = false;
            th = null;
            connect ();
        }

        // if the connection has already been bound and need to be rebinded
        if (bound && rebind) {
            // if the thread has more than one LDAPConnection, then the connection
            // should disconnect first. Otherwise, we can reuse the same thread
            // and do the rebind again.
            if (th.getClientCount() > 1) {
                disconnect();
                connect();
            }
        }

        if ((bound && rebind) || (!bound)) {
            try {
                // Get the initial request to start authentication
                Object[] arg1 = new Object[5];
                arg1[0] = boundDN;
                arg1[1] = "LDAP";
                arg1[2] = null;
                arg1[3] = m_securityProperties;
                arg1[4] = m_clientCB;

                String[] argNames = new String[5];
                argNames[0] = "java.lang.String";
                argNames[1] = "java.lang.String";
                argNames[2] = "java.lang.String";
                argNames[3] = "java.util.Properties";
                argNames[4] = "com.netscape.sasl.SASLClientCB";

                byte[] outVals = (byte[])invokeMethod(m_mechanismDriver,
                  m_mechanismDriver.getClass().getName(),
                  "startAuthentication", arg1, argNames);


                int resultCode = LDAPException.SASL_BIND_IN_PROGRESS;
                JDAPBindResponse response = null;
                while (!checkForSASLBindCompletion(resultCode)) {
                    response = saslBind(outVals);
                    resultCode = response.getResultCode();
                    String challenge = response.getCredentials();
                    byte[] b = challenge.getBytes();

                    Object[] arg2 = new Object[1];
                    arg2[0] = b;
                    String[] arg2Names = new String[1];
                    arg2Names[0] = "[B";  //class name for byte array

                    outVals = (byte[])invokeMethod(m_mechanismDriver,
                        m_mechanismDriver.getClass().getName(),
                        "evaluateResponse", arg2, arg2Names);
                }

                // Make sure authentication REALLY is complete
                Boolean bool = (Boolean)invokeMethod(m_mechanismDriver,
                    m_mechanismDriver.getClass().getName(),
                    "isComplete", null, null);
                if (!bool.booleanValue()) {
                    // Authentication session hijacked!
                    throw new LDAPException("The server indicates that " +
                        "authentication is successful, but the SASL driver " +
                        "indicates that authentication is not yet done.",
                        LDAPException.OTHER);
                }

                m_security = invokeMethod(m_mechanismDriver,
                    m_mechanismDriver.getClass().getName(),
                    "getSecurityLayer", null, null);
                th.setSecurityLayer(m_security);
                updateThreadConnTable();
            } catch (Exception e) {
                throw new LDAPException(e.toString(), LDAPException.OTHER);
            }
        }
    }

    private Object invokeMethod(Object obj, String packageName,
      String methodName, Object[] args, String[] argNames)
      throws LDAPException {
        try {
            java.lang.reflect.Method m = getMethod(packageName, methodName,
              argNames);
            if (m != null) {
                return (m.invoke(obj, args));
            }
        } catch (Exception e) {
            throw new LDAPException("Invoking "+methodName+": "+
              e.toString(), LDAPException.PARAM_ERROR);
        }

        return null;
    }

    private java.lang.reflect.Method getMethod(String packageName,
      String methodName, String[] args) throws LDAPException {
        try {
            java.lang.reflect.Method method = null;
            String suffix = "";
            if (args != null)
                for (int i=0; i<args.length; i++)
                    suffix = suffix+args[i].getClass().getName();
            String key = packageName+"."+methodName+"."+suffix;
            if ((method = (java.lang.reflect.Method)(m_methodLookup.get(key)))
              != null)
                return method;

            Class c = Class.forName(packageName);
            java.lang.reflect.Method[] m = c.getMethods();
            for (int i = 0; i < m.length; i++ ) {
                Class[] params = m[i].getParameterTypes();
                if ((m[i].getName().equals(methodName)) &&
                    signatureCorrect(params, args)) {
                    m_methodLookup.put(key, m[i]);
                    return m[i];
                }
            }
            throw new LDAPException("Method " + methodName + " not found in " +
              packageName);
        } catch (ClassNotFoundException e) {
            throw new LDAPException("Class "+ packageName + " not found");
        }
    }

    private boolean signatureCorrect(Class params[], String args[]) {
        if (args == null)
            return true;
        if (params.length != args.length)
            return false;
        for (int i=0; i<params.length; i++) {
            if (!params[i].getName().equals(args[i]))
                return false;
        }
        return true;
    }

    private boolean checkForSASLBindCompletion(int resultCode)
      throws LDAPException{

        if (resultCode == LDAPException.SUCCESS) {
            return true;
        } else if (resultCode == LDAPException.SASL_BIND_IN_PROGRESS) {
            return false;
        } else {
            throw new LDAPException("sasl", resultCode);
        }
    }

    private JDAPBindResponse saslBind(byte[] credentials) throws LDAPException {
        saslBind = true;
        if (th == null) connect ();

        LDAPResponseListener myListener = getResponseListener ();
        JDAPMessage response;

        try {
            String name = (String)invokeMethod(m_mechanismDriver,
              m_mechanismDriver.getClass().getName(), "getMechanismName",
              null, null);
            sendRequest(new JDAPBindRequest(protocolVersion, boundDN,
              name, credentials),
              myListener, defaultConstraints);
            response = myListener.getResponse();

            JDAPProtocolOp protocolOp = response.getProtocolOp();
            if (protocolOp instanceof JDAPBindResponse)
                return (JDAPBindResponse)protocolOp;
            else
                throw new LDAPException("Unknown response from the server during SASL bind",
                  LDAPException.OTHER);
        } finally {
            releaseResponseListener(myListener);
        }
    }

    /**
     * Internal routine. Binds to the LDAP server.
     * @param version protocol version to request from server
     * @param rebind True if the rebind operation is requested, otherwise, false.
     * @exception LDAPException failed to bind
     */
    private void bind (int version, boolean rebind,
      LDAPSearchConstraints cons) throws LDAPException {
        saslBind = false;

        if (th == null) {
            bound = false;
            th = null;
            connect ();
            // special case: the connection currently is not bound, and now there is
            // a bind request. The connection needs to reconnect if the thread th has
            // more than one LDAPConnection.
        } else if (!bound && rebind && th.getClientCount() > 1) {
            disconnect();
            connect();
        }

        // if the connection is still intact and no rebind request
        if (bound && !rebind)
            return;

        // if the connection was lost and did not have any kind of bind
        // operation and the current one does not request any bind operation (ie,
        // no authenticate has been called)
        if (!m_anonymousBound &&
         ((boundDN == null) || (boundPasswd == null)) &&
         !rebind)
            return;

        if (bound && rebind) {
            if (protocolVersion == version) {
                if (m_anonymousBound && ((boundDN == null) || (boundPasswd == null)))
                    return;

                if (!m_anonymousBound && (boundDN != null) && (boundPasswd != null) &&
                    boundDN.equals(prevBoundDN) &&
                    boundPasswd.equals(prevBoundPasswd))
                    return;
            }

            // if get to here, means that we need to do rebind since previous and
            // current credentials are not the same.
            // if the current connection is the only connection for the th thread,
            // then reuse this current connection. otherwise, disconnect the current
            // one (ie, detach from the current th) and reconnect again (ie, have
            // a new th).
            if (th.getClientCount() > 1) {
                disconnect();
                connect();
            }
        }

        protocolVersion = version;

        LDAPResponseListener myListener = getResponseListener ();
        JDAPMessage response;
        try {
            if (m_referralConnection != null) {
                m_referralConnection.disconnect();
                m_referralConnection = null;
            }
            sendRequest(new JDAPBindRequest(protocolVersion, boundDN, boundPasswd),
                myListener, cons);
            response = myListener.getResponse();
            checkMsg(response);
        } catch (LDAPReferralException e) {
            m_referralConnection = createReferralConnection(e, cons);
        } finally {
            releaseResponseListener(myListener);
        }

        updateThreadConnTable();
    }

    private void updateThreadConnTable() {
        synchronized(m_threadConnTable) {
            if (m_threadConnTable.containsKey(th)) {
                Vector v = (Vector)m_threadConnTable.get(th);
                for (int i=0, n=v.size(); i<n; i++) {
                    LDAPConnection conn = (LDAPConnection)v.elementAt(i);
                    conn.bound = true;
                }
            } else
                printDebug("Thread table does not contain the specified thread");
        }
    }

    private void sendRequest(JDAPProtocolOp oper, LDAPResponseListener myListener,
      LDAPSearchConstraints cons) throws LDAPException {

        // retry three times if we got the nullPointer exception.
        // Dont remove this. The following situation might happen:
        // Before sendRequest get invoked, it is possible that the LDAPConnThread
        // encountered the networkError and called the deregisterConnection which
        // in turn set the "th" thread to be null.
        for (int i=0; i<3; i++) {
            try {
                th.sendRequest(oper, myListener, cons);
                break;
            } catch(NullPointerException ne) {
                // do nothing
            }
        }
        if (th == null)
            throw new LDAPException("The connection is not available",
                LDAPException.OTHER);
    }

    /**
     * Internal routine. Binds to the LDAP server.
     * @exception LDAPException failed to bind
     */
    private void bind (LDAPSearchConstraints cons) throws LDAPException {
        if (saslBind)
            saslBind(false);
        else
            bind (protocolVersion, false, cons);
    }

    /**
     * Disconnects from the LDAP server. Before you can perform LDAP operations
     * again, you need to reconnect to the server by calling
     * <CODE>connect</CODE>.
     * @exception LDAPException Failed to disconnect from the LDAP server.
     * @see netscape.ldap.LDAPConnection#connect(java.lang.String, int)
     * @see netscape.ldap.LDAPConnection#connect(java.lang.String, int, java.lang.String, java.lang.String)
     */
    public synchronized void disconnect() throws LDAPException {
        if (m_referralConnection != null) {
            m_referralConnection.disconnect();
            m_referralConnection = null;
        }

        if (th == null)
            throw new LDAPException ( "unable to disconnect() without connecting",
                                  LDAPException.OTHER );
        if (m_cache != null) {
            m_cache.cleanup();
            m_cache = null;
        }
        deleteThreadConnEntry();
        deregisterConnection();
    }

    private void deleteThreadConnEntry() {
        synchronized (m_threadConnTable) {
            Enumeration keys = m_threadConnTable.keys();
            while (keys.hasMoreElements()) {
                LDAPConnThread connThread = (LDAPConnThread)keys.nextElement();
                Vector connVector = (Vector)m_threadConnTable.get(connThread);
                Enumeration enumv = connVector.elements();
                while (enumv.hasMoreElements()) {
                    LDAPConnection c = (LDAPConnection)enumv.nextElement();
                    if (c.equals(this)) {
                        connVector.removeElement(c);
                        if (connVector.size() == 0)
                            m_threadConnTable.remove(connThread);
                        return;
                    }
                }
            }
        }
    }

    /**
     *  Decrement the reference count for the connection
     */
    synchronized void deregisterConnection() {
        th.deregister(this);
        th = null;
        bound = false;
    }

    /**
     * Reads the entry for the specified distiguished name (DN) and retrieves all
     * attributes for the entry.
     * <P>
     *
     * For example, the following section of code reads the entry for
     * Barbara Jensen and retrieves all attributes for that entry.
     * <P>
     *
     * <PRE>
     * String findDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * LDAPEntry foundEntry = null;
     * try {
     *     foundEntry = myConn.read( findDN );
     * } catch ( LDAPException e ) {
     *     switch( e.getLDAPResultCode() ) {
     *         case e.NO_SUCH_OBJECT:
     *             System.out.println( "The specified entry does not exist." );
     *             break;
     *         case e.LDAP_PARTIAL_RESULTS:
     *             System.out.println( "Entry served by a different LDAP server." );
     *             break;
     *         case e.INSUFFICIENT_ACCESS_RIGHTS:
     *             System.out.println( "You do not have the access rights to perform this operation." );
     *             break;
     *         default:
     *             System.out.println( "Error number: " + e.getLDAPResultCode() );
     *             System.out.println( "Could not read the specified entry." );
     *             break;
     *     }
     *     return;
     * }
     * System.out.println( "Found the specified entry." );
     * </PRE>
     *
     * @param DN Distinguished name of the entry that you want to retrieve.
     * @exception LDAPException Failed to find or read the specified entry
     * from the directory.
     * @return LDAPEntry Returns the specified entry or raises an exception
     * if the entry is not found.
     */
    public LDAPEntry read (String DN) throws LDAPException {
        return read (DN, null, defaultConstraints);
    }

    /**
     * Reads the entry for the specified distiguished name (DN) and retrieves all
     * attributes for the entry. This method allows the user to specify the
     * preferences for the read operation.
     * <P>
     *
     * For example, the following section of code reads the entry for
     * Barbara Jensen and retrieves all attributes for that entry.
     * <P>
     *
     * <PRE>
     * String findDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * LDAPEntry foundEntry = null;
     * try {
     *     foundEntry = myConn.read( findDN );
     * } catch ( LDAPException e ) {
     *     switch( e.getLDAPResultCode() ) {
     *         case e.NO_SUCH_OBJECT:
     *             System.out.println( "The specified entry does not exist." );
     *             break;
     *         case e.LDAP_PARTIAL_RESULTS:
     *             System.out.println( "Entry served by a different LDAP server." );
     *             break;
     *         case e.INSUFFICIENT_ACCESS_RIGHTS:
     *             System.out.println( "You do not have the access rights to perform this operation." );
     *             break;
     *         default:
     *             System.out.println( "Error number: " + e.getLDAPResultCode() );
     *             System.out.println( "Could not read the specified entry." );
     *             break;
     *     }
     *     return;
     * }
     * System.out.println( "Found the specified entry." );
     * </PRE>
     *
     * @param DN Distinguished name of the entry that you want to retrieve.
     * @param cons Preferences for the read operation.
     * @exception LDAPException Failed to find or read the specified entry
     * from the directory.
     * @return LDAPEntry Returns the specified entry or raises an exception
     * if the entry is not found.
     */
    public LDAPEntry read (String DN, LDAPSearchConstraints cons)
        throws LDAPException {
        return read (DN, null, cons);
    }

    /**
     * Reads the entry for the specified distinguished name (DN) and
     * retrieves only the specified attributes from the entry.
     * <P>
     *
     * For example, the following section of code reads the entry for
     * Barbara Jensen and retrieves only the <CODE>cn</CODE> and
     * <CODE>sn</CODE> attributes.
     * The example prints out all attributes that have been retrieved
     * (the two specified attributes).
     * <P>
     *
     * <PRE>
     * String findDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * LDAPEntry foundEntry = null;
     * String getAttrs[] = { "cn", "sn" };
     * try {
     *      foundEntry = myConn.read( findDN, getAttrs );
     * } catch ( LDAPException e ) {
     *      switch( e.getLDAPResultCode() ) {
     *           case e.NO_SUCH_OBJECT:
     *               System.out.println( "The specified entry does not exist." );
     *               break;
     *           case e.LDAP_PARTIAL_RESULTS:
     *               System.out.println( "Entry served by a different LDAP server." );
     *               break;
     *           case e.INSUFFICIENT_ACCESS_RIGHTS:
     *               System.out.println( "You do not have the access " +
     *                                   "rights to perform this operation." );
     *               break;
     *           default:
     *               System.out.println( "Error number: " + e.getLDAPResultCode() );
     *               System.out.println( "Could not read the specified entry." );
     *               break;
     *      }
     *      return;
     * }
     *
     * LDAPAttributeSet foundAttrs = foundEntry.getAttributeSet();
     * int size = foundAttrs.size();
     * Enumeration enumAttrs = foundAttrs.getAttributes();
     * System.out.println( "Attributes: " );
     *
     * while ( enumAttrs.hasMoreElements() ) {
     *      LDAPAttribute anAttr = ( LDAPAttribute )enumAttrs.nextElement();
     *      String attrName = anAttr.getName();
     *      System.out.println( "\t" + attrName );
     *      Enumeration enumVals = anAttr.getStringValues();
     *      while ( enumVals.hasMoreElements() ) {
     *           String aVal = ( String )enumVals.nextElement();
     *           System.out.println( "\t\t" + aVal );
     *      }
     * }
     * </PRE>
     *
     * @param DN Distinguished name of the entry that you want to retrieve.
     * @param attrs Names of attributes that you want to retrieve.
     * @return LDAPEntry Returns the specified entry (or raises an
     * exception if the entry is not found).
     * @exception LDAPException Failed to read the specified entry from
     * the directory.
     */
    public LDAPEntry read (String DN, String attrs[]) throws LDAPException {
        return read(DN, attrs, defaultConstraints);
    }

    public LDAPEntry read (String DN, String attrs[],
        LDAPSearchConstraints cons) throws LDAPException {
        LDAPSearchResults results = search (DN, LDAPv2.SCOPE_BASE,
          defaultFilter, attrs, false, cons);
        if (results == null)
            return null;
        return results.next ();
    }

    /**
     * Reads the entry specified by the LDAP URL. <P>
     *
     * When you call this method, a new connection is created automatically,
     * using the host and port specified in the URL. After finding the entry,
     * the method closes this connection (in other words, it disconnects from
     * the LDAP server). <P>
     *
     * If the URL specifies a filter and scope, these are not used.
     * Of the information specified in the URL, this method only uses
     * the LDAP host name and port number, the base distinguished name (DN),
     * and the list of attributes to return. <P>
     *
     * The method returns the entry specified by the base DN. <P>
     *
     * (Note: If you want to search for more than one entry, use the
     * <CODE>search( LDAPUrl )</CODE> method instead.) <P>
     *
     * For example, the following section of code reads the entry specified
     * by the LDAP URL.
     * <P>
     *
     * <PRE>
     * String flatURL = "ldap://alway.mcom.com:3890/cn=Barbara Jenson,ou=Product Development,o=Ace Industry,c=US?cn,sn,mail";
     * LDAPUrl myURL;
     * try {
     *    myURL = new LDAPUrl( flatURL );
     * } catch ( java.net.MalformedURLException e ) {
     *    System.out.println( "BAD URL!!!  BAD, BAD, BAD URL!!!" );
     *    return;
     * }
     * LDAPEntry myEntry = null;
     * try {
     *    myEntry = myConn.read( myURL );
     * } catch ( LDAPException e ) {
     *    int errCode = e.getLDAPResultCode();
     *    switch( errCode ) {
     *        case ( e.NO_SUCH_OBJECT ):
     *            System.out.println( "The specified entry " + myDN +
     *                                " does not exist in the directory." );
     *            return;
     *        default:
     *            System.out.println( "An internal error occurred." );
     *            return;
     *    }
     * }
     * </PRE>
     *
     * @param toGet LDAP URL specifying the entry that you want to read.
     * @return LDAPEntry Returns the entry specified by the URL (or raises
     * an exception if the entry is not found).
     * @exception LDAPException Failed to read the specified entry from
     * the directory.
     * @see netscape.ldap.LDAPUrl
     * @see netscape.ldap.LDAPConnection#search(netscape.ldap.LDAPUrl)
     */
    public static LDAPEntry read (LDAPUrl toGet) throws LDAPException {
        String host = toGet.getHost ();
        int port = toGet.getPort();

        if (host == null)
            throw new LDAPException ( "no host for connection",
                                    LDAPException.PARAM_ERROR );

        String[] attributes = toGet.getAttributeArray ();
        String DN = toGet.getDN();
        LDAPEntry returnValue;

        LDAPConnection connection = new LDAPConnection ();
        connection.connect (host, port);

        returnValue = connection.read (DN, attributes);
        connection.disconnect ();

        return returnValue;
    }

    /**
     * Performs the search specified by the LDAP URL. <P>
     *
     * For example, the following section of code searches for all entries under
     * the <CODE>ou=Product Development,o=Ace Industry,c=US</CODE> subtree of a
     * directory.  The example gets and prints the mail attribute for each entry
     * found. <P>
     *
     * <PRE>
     * String flatURL = "ldap://alway.mcom.com:3890/ou=Product Development,o=Ace Industry,c=US?mail?sub?objectclass=*";
     * LDAPUrl myURL;
     * try {
     *    myURL = new LDAPUrl( flatURL );
     * } catch ( java.net.MalformedURLException e ) {
     *    System.out.println( "Incorrect URL syntax." );
     *    return;
     * }
     *
     * LDAPSearchResults myResults = null;
     * try {
     *    myResults = myConn.search( myURL );
     * } catch ( LDAPException e ) {
     *    int errCode = e.getLDAPResultCode();
     *    System.out.println( "LDAPException: return code:" + errCode );
     *    return;
     * }
     *
     * while ( myResults.hasMoreElements() ) {
     *    LDAPEntry myEntry = myResults.next();
     *    String nextDN = myEntry.getDN();
     *    System.out.println( nextDN );
     *    LDAPAttributeSet entryAttrs = myEntry.getAttributeSet();
     *    Enumeration attrsInSet = entryAttrs.getAttributes();
     *    while ( attrsInSet.hasMoreElements() ) {
     *        LDAPAttribute nextAttr = (LDAPAttribute)attrsInSet.nextElement();
     *        String attrName = nextAttr.getName();
     *        System.out.print( "\t" + attrName + ": " );
     *        Enumeration valsInAttr = nextAttr.getStringValues();
     *        while ( valsInAttr.hasMoreElements() ) {
     *            String nextValue = (String)valsInAttr.nextElement();
     *            System.out.println( nextValue );
     *        }
     *    }
     * }
     * </PRE>
     * <P>
     *
     * To abandon the search, use the <CODE>abandon</CODE> method.
     *
     * @param toGet LDAP URL representing the search that you want to perform.
     * @return LDAPSearchResults The results of the search as an enumeration.
     * @exception LDAPException Failed to complete the search specified by
     * the LDAP URL.
     * @see netscape.ldap.LDAPUrl
     * @see netscape.ldap.LDAPSearchResults
     * @see netscape.ldap.LDAPConnection#abandon(netscape.ldap.LDAPSearchResults)
     */
    public static LDAPSearchResults search (LDAPUrl toGet) throws LDAPException {
        return search (toGet, null);
    }

    /**
     * Performs the search specified by the LDAP URL. This method also
     * allows you to specify constraints for the search (such as the
     * maximum number of entries to find or the
     * maximum time to wait for search results). <P>
     *
     * As part of the search constraints, you can specify whether or not you
     * want the results delivered all at once or in smaller batches.
     * If you specify that you want the results delivered in smaller
     * batches, each iteration blocks until the next batch of results is
     * returned. <P>
     *
     * For example, the following section of code retrieves the first 5
     * matching entries for the search specified by the LDAP URL.  The
     * example accomplishes this by creating a new set of search
     * constraints where the maximum number of search results is 5. <P>
     *
     * <PRE>
     * LDAPSearchConstraints mySearchConstraints = myConn.getSearchConstraints();
     * mySearchConstraints.setMaxResults( 5 );
     * String flatURL = "ldap://alway.mcom.com:3890/ou=Product Development,o=Ace Industry,c=US?mail?sub?objectclass=*";
     * LDAPUrl myURL;
     * try {
     *    myURL = new LDAPUrl( flatURL );
     * } catch ( java.net.MalformedURLException e ) {
     *    System.out.println( "Incorrect URL syntax." );
     *    return;
     * }
     * LDAPSearchResults myResults = null;
     * try {
     *    myResults = myConn.search( myURL, mySearchConstraints );
     * } catch ( LDAPException e ) {
     *    int errCode = e.getLDAPResultCode();
     *    System.out.println( "LDAPException: return code:" + errCode );
     *    return;
     * }
     * </PRE>
     * <P>
     *
     * To abandon the search, use the <CODE>abandon</CODE> method.
     *
     * @param toGet LDAP URL representing the search that you want to run.
     * @param cons Constraints specific to the search.
     * @return LDAPSearchResults The results of the search as an enumeration.
     * @exception LDAPException Failed to complete the search specified
     * by the LDAP URL.
     * @see netscape.ldap.LDAPUrl
     * @see netscape.ldap.LDAPSearchResults
     * @see netscape.ldap.LDAPConnection#abandon(netscape.ldap.LDAPSearchResults)
     */
    public static LDAPSearchResults search (LDAPUrl toGet,
        LDAPSearchConstraints cons) throws LDAPException {
        String host = toGet.getHost ();
        int port = toGet.getPort();

        if (host == null)
            throw new LDAPException ( "no host for connection",
                                    LDAPException.PARAM_ERROR );

        String[] attributes = toGet.getAttributeArray ();
        String DN = toGet.getDN();
        String filter = toGet.getFilter();
        if (filter == null) {
            filter = "(objectClass=*)";
        }
        int scope = toGet.getScope ();

        LDAPConnection connection = new LDAPConnection ();
        connection.connect (host, port);

        LDAPSearchResults results;
        if (cons != null)
            results = connection.search (DN, scope, filter, attributes, false, cons);
        else
            results = connection.search (DN, scope, filter, attributes, false);

        results.closeOnCompletion(connection);

        return results;
    }

    /**
     * Performs the search specified by the criteria that you enter. <P>
     *
     * For example, the following section of code searches for all entries under
     * the <CODE>ou=Product Development,o=Ace Industry,c=US</CODE> subtree of a
     * directory.  The example gets and prints the mail attribute for each entry
     * found. <P>
     *
     * <PRE>
     * String myBaseDN = "ou=Product Development,o=Ace Industry,c=US";
     * String myFilter="(objectclass=*)";
     * String[] myAttrs = { "mail" };
     *
     * LDAPSearchResults myResults = null;
     * try {
     *    myResults = myConn.search( myBaseDN, LDAPv2.SCOPE_SUB, myFilter, myAttrs, false );
     * } catch ( LDAPException e ) {
     *    int errCode = e.getLDAPResultCode();
     *    System.out.println( "LDAPException: return code:" + errCode );
     *    return;
     * }
     *
     * while ( myResults.hasMoreElements() ) {
     *    LDAPEntry myEntry = myResults.next();
     *    String nextDN = myEntry.getDN();
     *    System.out.println( nextDN );
     *    LDAPAttributeSet entryAttrs = myEntry.getAttributeSet();
     *    Enumeration attrsInSet = entryAttrs.getAttributes();
     *    while ( attrsInSet.hasMoreElements() ) {
     *        LDAPAttribute nextAttr = (LDAPAttribute)attrsInSet.nextElement();
     *        String attrName = nextAttr.getName();
     *        System.out.println( "\t" + attrName + ":" );
     *        Enumeration valsInAttr = nextAttr.getStringValues();
     *        while ( valsInAttr.hasMoreElements() ) {
     *            String nextValue = (String)valsInAttr.nextElement();
     *            System.out.println( "\t\t" + nextValue );
     *        }
     *    }
     * }
     * </PRE>
     * <P>
     *
     * To abandon the search, use the <CODE>abandon</CODE> method.
     *
     * @param base The base distinguished name to search from
     * @param scope The scope of the entries to search.  You can specify one
     * of the following: <P>
     * <UL>
     * <LI><CODE>LDAPv2.SCOPE_BASE</CODE> (search only the base DN) <P>
     * <LI><CODE>LDAPv2.SCOPE_ONE</CODE>
     * (search only entries under the base DN) <P>
     * <LI><CODE>LDAPv2.SCOPE_SUB</CODE>
     * (search the base DN and all entries within its subtree) <P>
     * </UL>
     * <P>
     * @param filter Search filter specifying the search criteria.
     * @param attrs List of attributes that you want returned in the
     * search results.
     * @param attrsOnly If true, returns the names but not the values of the
     * attributes found.  If false, returns the names and values for
     * attributes found
     * @return LDAPSearchResults The results of the search as an enumeration.
     * @exception LDAPException Failed to complete the specified search.
     * @see netscape.ldap.LDAPConnection#abandon(netscape.ldap.LDAPSearchResults)
     */
    public LDAPSearchResults search( String base, int scope, String filter,
        String[] attrs, boolean attrsOnly ) throws LDAPException {
        return search( base, scope, filter, attrs, attrsOnly, defaultConstraints);
    }

    /**
     * Performs the search specified by the criteria that you enter.
     * This method also allows you to specify constraints for the search
     * (such as the maximum number of entries to find or the
     * maximum time to wait for search results). <P>
     *
     * As part of the search constraints, you can specify whether or not
     * you want the
     * results delivered all at once or in smaller batches.  If you
     * specify that you want the results delivered in smaller batches,
     * each iteration blocks until the
     * next batch of results is returned. <P>
     *
     * For example, the following section of code retrieves the first 5 entries
     * matching the specified search criteria.  The example accomplishes
     * this by creating a new set of search constraints where the maximum
     * number of search results is 5. <P>
     *
     * <PRE>
     * String myBaseDN = "ou=Product Development,o=Ace Industry,c=US";
     * String myFilter="(objectclass=*)";
     * String[] myAttrs = { "mail" };
     * LDAPSearchConstraints mySearchConstraints = myConn.getSearchConstraints();
     * mySearchConstraints.setMaxResults( 5 );
     *
     * LDAPSearchResults myResults = null;
     * try {
     *    myResults = myConn.search( myBaseDN, LDAPv2.SCOPE_SUB, myFilter, myAttrs, false, mySearchConstraints );
     * } catch ( LDAPException e ) {
     *    int errCode = e.getLDAPResultCode();
     *    System.out.println( "LDAPException: return code:" + errCode );
     *    return;
     * }
     * </PRE>
     * <P>
     *
     * To abandon the search, use the <CODE>abandon</CODE> method.
     *
     * @param base The base distinguished name to search from
     * @param scope The scope of the entries to search.  You can specify one
     * of the following: <P>
     * <UL>
     * <LI><CODE>LDAPv2.SCOPE_BASE</CODE> (search only the base DN) <P>
     * <LI><CODE>LDAPv2.SCOPE_ONE</CODE>
     * (search only entries under the base DN) <P>
     * <LI><CODE>LDAPv2.SCOPE_SUB</CODE>
     * (search the base DN and all entries within its subtree) <P>
     * </UL>
     * <P>
     * @param filter Search filter specifying the search criteria.
     * @param attrs List of attributes that you want returned in the search
     * results.
     * @param cons Constraints specific to this search (for example, the
     * maximum number
     * of entries to return).
     * @param attrsOnly If true, returns the names but not the values of the
     * attributes found.  If false, returns the names and  values for
     * attributes found
     * @return LDAPSearchResults The results of the search as an enumeration.
     * @exception LDAPException Failed to complete the specified search.
     * @see netscape.ldap.LDAPConnection#abandon(netscape.ldap.LDAPSearchResults)
     */
    public LDAPSearchResults search( String base, int scope, String filter,
        String[] attrs, boolean attrsOnly, LDAPSearchConstraints cons )
        throws LDAPException {

        if (cons == null)
            cons = defaultConstraints;

        LDAPSearchResults returnValue = new LDAPSearchResults(this,
            cons, base, scope, filter, attrs, attrsOnly);
        Vector cacheValue = null;
        Long key = null;
        boolean isKeyValid = true;

        try {
            // get entry from cache which is a vector of JDAPMessages
            if (m_cache != null)
            {
                // create key for cache entry using search arguments
                key = m_cache.createKey(host, port, base, filter, scope, attrs, boundDN, cons);

                cacheValue = (Vector)m_cache.getEntry(key);

                if (cacheValue != null)
                    return (new LDAPSearchResults(cacheValue, this, cons, base, scope,
                        filter, attrs, attrsOnly));
            }
        } catch (LDAPException e) {
            isKeyValid = false;
            printDebug("Exception: "+e);
        }

        bind(cons);

        LDAPSearchListener myListener = getSearchListener ( cons );
        int deref = cons.getDereference();

        JDAPSearchRequest request = new JDAPSearchRequest (base,
            scope, deref, cons.getMaxResults(), cons.getTimeLimit(),
            attrsOnly, filter, attrs);

        synchronized(myListener) {
            boolean success = false;
            try {
                sendRequest (request, myListener, cons);
                success = true;
            } finally {
                if (!success)
                releaseSearchListener (myListener);
            }

            // if using cache, then need to add the key to the search listener.
            // The search listener retrieves the key and then add the key and
            // a vector of results to the hashtable.
            if ((m_cache != null) && (isKeyValid))
                myListener.setKey(key);
        }


        /* Synchronous search if all requested at once */
        if ( cons.getBatchSize() == 0 ) {
            try {
                /* Block until all results are in */
                JDAPMessage response = myListener.getResponse ();
                Enumeration results = myListener.getSearchResults ();

                try {
                    checkSearchMsg(returnValue, response, cons, base, scope,
                                   filter, attrs, attrsOnly);
                } catch ( LDAPException ex ) {
                    /* Was the exception caused by a bad referral? */
                    JDAPProtocolOp op = response.getProtocolOp();
                    if ( (op instanceof JDAPSearchResultReference) ||
                         (op instanceof JDAPSearchResult) ){
                        System.err.println( "LDAPConnection.checkSearchMsg: " +
                                            "ignoring bad referral" );
                    } else {
                        throw ex;
                    }
                }

                while (results.hasMoreElements ()) {
                    JDAPMessage msg = (JDAPMessage)results.nextElement();

                    checkSearchMsg(returnValue, msg, cons, base, scope, filter, attrs,
                        attrsOnly);
                }
            } catch (LDAPException ee) {
                throw ee;
            } finally {
                releaseSearchListener (myListener);
            }
        } else {
            /*
            * Asynchronous to retrieve one at a time, check to make sure
            * the search didn't fail
            */
            JDAPMessage firstResult = myListener.nextResult ();
            if (firstResult == null) {
                firstResult = myListener.getResponse ();

                try {
                    checkSearchMsg(returnValue, firstResult, cons, base, scope,
                      filter, attrs, attrsOnly);
                } finally {
                    releaseSearchListener (myListener);
                }
            } else {
                /* First result could be a bad referral, ultimately causing
                   a NO_SUCH_OBJECT exception. Don't want to miss all
                   following results, so skip it. */
                try {
                    checkSearchMsg(returnValue, firstResult, cons, base,
                                   scope, filter, attrs, attrsOnly);
                } catch ( LDAPException ex ) {
                    /* Was the exception caused by a bad referral? */
                    if (firstResult.getProtocolOp() instanceof
                        JDAPSearchResultReference) {
                        System.err.println( "LDAPConnection.checkSearchMsg: " +
                                            "ignoring bad referral" );
                    } else {
                        throw ex;
                    }
                }

                LDAPControl[] controls = (LDAPControl[])getOption(LDAPv3.SERVERCONTROLS, cons);

                for (int i=0; (controls != null) && (i<controls.length); i++) {
                    if ((controls[i].getClass().getName()).equals(PersistSearchPackageName))
                    {
                        returnValue.associatePersistentSearch (myListener);
                        return returnValue;
                    }
                }
                /* we let this listener get garbage collected.. */
                returnValue.associate (myListener);
            }
        }
        return returnValue;
    }

    void checkSearchMsg(LDAPSearchResults value, JDAPMessage msg,
        LDAPSearchConstraints cons, String dn, int scope, String filter,
        String attrs[], boolean attrsOnly) throws LDAPException {
        try {
            checkMsg (msg);
            // not the JDAPResult
            if (msg.getProtocolOp().getType() != JDAPProtocolOp.SEARCH_RESULT) {
                value.add (msg.getProtocolOp());
            }
        } catch (LDAPReferralException e) {
            Vector res = new Vector();
            performReferrals(e, cons, JDAPProtocolOp.SEARCH_REQUEST, dn,
                scope, filter, attrs, attrsOnly, null, null, null, res);

            // the size of the vector can be more than 1 because it is possible
            // to visit more than one referral url to retrieve the entries
            for (int i=0; i<res.size(); i++)
                value.addReferralEntries((LDAPSearchResults)res.elementAt(i));

            res = null;
        } catch (LDAPException e) {
            if ((e.getLDAPResultCode() == LDAPException.ADMIN_LIMIT_EXCEEDED) ||
                (e.getLDAPResultCode() == LDAPException.TIME_LIMIT_EXCEEDED) ||
                (e.getLDAPResultCode() == LDAPException.SIZE_LIMIT_EXCEEDED)) {
                value.add(e);
            } else
                throw e;
        }
    }

    /**
     * Checks to see if an entry contains an attribute with a specified value.
     * Returns <CODE>true</CODE> if the entry has the value. Returns
     * <CODE>false</CODE> if the entry does not have the value or the
     * attribute. To represent the value that you want compared, you need
     * to create an <CODE>LDAPAttribute</CODE> object.<P>
     *
     * Note that only string values can be compared. <P>
     *
     * For example, the following section of code checks to see if the entry
     * "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US" contains
     * the attribute "mail" with the value "bjensen@aceindustry.com".
     *
     * <PRE>
     * ...
     * LDAPConnection myConn = new LDAPConnection();
     * ...
     * String myDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * String nameOfAttr = "mail";
     * String valOfAttr = "bjensen@aceindustry.com";
     * LDAPAttribute cmpThisAttr = new LDAPAttribute( nameOfAttr, valOfAttr );
     * boolean hasValue = myConn.compare( myDN, cmpThisAttr );
     * if ( hasValue ) {
     *     System.out.println( "Attribute and value found in entry." );
     * } else {
     *     System.out.println( "Attribute and value not found in entry." );
     * }
     * ...</PRE>
     *
     * @param DN The distinguished name of the entry that you want to use in
     * the comparison.
     * @param attr The attribute that you want to compare against the entry.
     * (The method checks to see if the entry has an attribute with the same name
     * and value as this attribute.)
     * @return true if the entry contains the specified attribute and value.
     * @exception LDAPException Failed to perform the comparison.
     * @see netscape.ldap.LDAPAttribute
     */
    public boolean compare( String DN, LDAPAttribute attr )
        throws LDAPException {
        return compare(DN, attr, defaultConstraints);
    }

    public boolean compare( String DN, LDAPAttribute attr,
        LDAPSearchConstraints cons) throws LDAPException {
        bind(cons);

        LDAPResponseListener myListener = getResponseListener ();
        Enumeration en = attr.getByteValues();
        byte val[] = (byte[])en.nextElement();
        String debug = "";
        try {
            debug = new String(val, "UTF8");
        } catch(Throwable x)
        {}

        JDAPAVA ass = new JDAPAVA(attr.getName(), debug);
        //JDAPAVA ass = new JDAPAVA(attr.getName(), new String(val,0));

        JDAPMessage response;
        try {
            sendRequest (new JDAPCompareRequest (DN, ass), myListener, cons);
            response = myListener.getResponse ();

            int resultCode = ((JDAPResult)response.getProtocolOp()).getResultCode();
            if (resultCode == JDAPResult.COMPARE_FALSE)
                return false;
            if (resultCode == JDAPResult.COMPARE_TRUE)
                return true;

            checkMsg (response);

        } catch (LDAPReferralException e) {
            Vector res = new Vector();
            performReferrals(e, cons, JDAPProtocolOp.COMPARE_REQUEST,
              DN, 0, null, null, false, null, null, attr, res);
            boolean bool = false;
            if (res.size() > 0)
               bool = ((Boolean)res.elementAt(0)).booleanValue();
            res = null;
            return bool;
        } finally {
            releaseResponseListener (myListener);
        }
        return false; /* this should never be executed */
    }

    /**
     * Adds an entry to the directory. <P>
     *
     * Before using this method, you need to create an
     * <CODE>LDAPEntry</CODE> object and use it to specify the
     * distinguished name and attributes of the new entry. Make sure
     * to specify values for all required attributes in the
     * entry. If all required attributes are not specified and the LDAP server
     * checks the entry against the schema, an <CODE>LDAPException</CODE>
     * may be thrown (where the LDAP result code is
     * <CODE>OBJECT_CLASS_VIOLATION</CODE>).<P>
     *
     * For example, the following section of code creates an
     * <CODE>LDAPEntry</CODE> object for a new entry and uses the object
     * to add the new entry to the directory. Because the definition of
     * the LDAP <CODE>inetOrgPerson</CODE> class specifies that the
     * <CODE>cn</CODE>, <CODE>sn</CODE>, and <CODE>objectclass</CODE>
     * attributes are required, these attributes are specified as part
     * of the new entry.  (<CODE>mail</CODE> is not required but is shown
     * here as an example of specifying additional attributes.)
     * <P>
     *
     * <PRE>
     * ...
     * String myDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     *
     * LDAPAttribute attr1 = new LDAPAttribute( "cn", "Barbara Jensen" );
     * LDAPAttribute attr2 = new LDAPAttribute( "sn", "Jensen" );
     * LDAPAttribute attr3 = new LDAPAttribute( "objectclass", "top" );
     * LDAPAttribute attr4 = new LDAPAttribute( "objectclass", "person" );
     * LDAPAttribute attr5 = new LDAPAttribute( "objectclass", "organizationalPerson" );
     * LDAPAttribute attr6 = new LDAPAttribute( "objectclass", "inetOrgPerson" );
     * LDAPAttribute attr7 = new LDAPAttribute( "mail", "bjensen@aceindustry.com" );
     *
     * LDAPAttributeSet myAttrs = new LDAPAttributeSet();
     * myAttrs.add( attr1 );
     * myAttrs.add( attr2 );
     * myAttrs.add( attr3 );
     * myAttrs.add( attr4 );
     * myAttrs.add( attr5 );
     * myAttrs.add( attr6 );
     * myAttrs.add( attr7 );
     *
     * LDAPEntry myEntry = new LDAPEntry( myDN, myAttrs );
     *
     * myConn.add( myEntry );
     * ... </PRE>
     *
     * @param entry LDAPEntry object specifying the distinguished name and
     * attributes of the new entry.
     * @exception LDAPException Failed to add the specified entry to the
     * directory.
     * @see netscape.ldap.LDAPEntry
     */
    public void add( LDAPEntry entry ) throws LDAPException {
        add(entry, defaultConstraints);
    }

    /**
     * Adds an entry to the directory and allows you to specify preferences
     * for this LDAP add operation by using an
     * <CODE>LDAPSearchConstraints</CODE> object. For
     * example, you can specify whether or not to follow referrals.
     * You can also apply LDAP v3 controls to the operation.
     * <P>
     *
     * @param entry LDAPEntry object specifying the distinguished name and
     * attributes of the new entry.
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to add the specified entry to the
     * directory.
     * @see netscape.ldap.LDAPEntry
     * @see netscape.ldap.LDAPSearchConstraints
     */
    public void add( LDAPEntry entry, LDAPSearchConstraints cons )
        throws LDAPException {
        bind (cons);

        LDAPResponseListener myListener = getResponseListener ();
        LDAPAttributeSet attrs = entry.getAttributeSet ();
        LDAPAttribute[] attrList = new LDAPAttribute[attrs.size()];
        for( int i = 0; i < attrs.size(); i++ )
            attrList[i] = (LDAPAttribute)attrs.elementAt( i );
        int attrPosition = 0;
        JDAPMessage response;
        try {
            sendRequest (new JDAPAddRequest (entry.getDN(), attrList),
                        myListener, cons);
            response = myListener.getResponse();
            checkMsg (response);
        } catch (LDAPReferralException e) {
            performReferrals(e, cons, JDAPProtocolOp.ADD_REQUEST,
                null, 0, null, null, false, null, entry, null, null);
        } finally {
            releaseResponseListener (myListener);
        }
    }

    /**
     * Performs an extended operation on the directory.  Extended operations
     * are part of version 3 of the LDAP protocol.<P>
     *
     * Note that in order for the extended operation to work, the server
     * that you are connecting to must support LDAP v3 and must be configured
     * to process the specified extended operation.
     *
     * @param op LDAPExtendedOperation object specifying the OID of the
     * extended operation and the data to be used in the operation.
     * @exception LDAPException Failed to execute the operation
     * @return LDAPExtendedOperation object representing the extended response
     * returned by the server.
     * @see netscape.ldap.LDAPExtendedOperation
     */
    public LDAPExtendedOperation extendedOperation( LDAPExtendedOperation op )
        throws LDAPException {

        return extendedOperation(op, defaultConstraints);
    }

    /**
     * Performs an extended operation on the directory.  Extended operations
     * are part of version 3 of the LDAP protocol. This method allows the
     * user to set the preferences for the operation.<P>
     *
     * Note that in order for the extended operation to work, the server
     * that you are connecting to must support LDAP v3 and must be configured
     * to process the specified extended operation.
     *
     * @param op LDAPExtendedOperation object specifying the OID of the
     * extended operation and the data to be used in the operation.
     * @param cons Preferences for the extended operation.
     * @exception LDAPException Failed to execute the operation
     * @return LDAPExtendedOperation object representing the extended response
     * returned by the server.
     * @see netscape.ldap.LDAPExtendedOperation
     */
    public LDAPExtendedOperation extendedOperation( LDAPExtendedOperation op,
      LDAPSearchConstraints cons) throws LDAPException {
        bind (cons);

        LDAPResponseListener myListener = getResponseListener ();
        JDAPMessage response = null;
        byte[] results = null;
        String resultID;

        try {
            sendRequest ( new JDAPExtendedRequest( op.getID(), op.getValue() ),
                         myListener, cons );
            response = myListener.getResponse();
            checkMsg (response);
            JDAPExtendedResponse res = (JDAPExtendedResponse)response.getProtocolOp();
            results = res.getValue();
            resultID = res.getID();
        } catch (LDAPReferralException e) {
            return performExtendedReferrals( e, cons, op );
        } finally {
            releaseResponseListener (myListener);
        }
        return new LDAPExtendedOperation( resultID, results );
    }

    /**
     * Makes a single change to an existing entry in the directory
     * (for example, changes the value of an attribute, adds a new
     * attribute value, or removes an existing attribute value). <P>
     *
     * Use the <CODE>LDAPModification</CODE> object to specify the change
     * that needs to be made and the <CODE>LDAPAttribute</CODE> object
     * to specify the attribute value that needs to be changed. The
     * <CODE>LDAPModification</CODE> object allows you add an attribute
     * value, change an attibute value, or remove an attribute
     * value. <P>
     *
     * For example, the following section of code changes Barbara Jensen's email
     * address in the directory to babs@aceindustry.com. <P>
     *
     * <PRE>
     * ...
     * String myEntryDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     *
     * LDAPAttribute attrEmail = new LDAPAttribute( "mail", "babs@aceindustry.com" );
     * LDAPModification singleChange = new LDAPModification( LDAPModification.REPLACE, attrEmail );
     *
     * myConn.modify( myEntryDN, singleChange );
     * ... </PRE>
     *
     * @param DN The distinguished name of the entry that you want to modify
     * @param mod A single change to be made to the entry
     * @exception LDAPException Failed to make the specified change to the
     * directory entry.
     * @see netscape.ldap.LDAPModification
     */
    public void modify( String DN, LDAPModification mod ) throws LDAPException {
        modify(DN, mod, defaultConstraints);
    }

   /**
    * Makes a single change to an existing entry in the directory and
    * allows you to specify preferences for this LDAP modify operation
    * by using an <CODE>LDAPSearchConstraints</CODE> object. For
    * example, you can specify whether or not to follow referrals.
    * You can also apply LDAP v3 controls to the operation.
    * <P>
    *
    * @param DN The distinguished name of the entry that you want to modify
    * @param mod A single change to be made to the entry
    * @param cons The set of preferences that you want applied to this operation.
    * @exception LDAPException Failed to make the specified change to the
    * directory entry.
    * @see netscape.ldap.LDAPModification
    * @see netscape.ldap.LDAPSearchConstraints
    */
    public void modify( String DN, LDAPModification mod,
        LDAPSearchConstraints cons ) throws LDAPException {
        LDAPModification[] mods = new LDAPModification [1];
        mods[0] = mod;
        modify (DN, mods, cons);
    }

    /**
     * Makes a set of changes to an existing entry in the directory
     * (for example, changes attribute values, adds new attribute values,
     * or removes existing attribute values). <P>
     *
     * Use the <CODE>LDAPModificationSet</CODE> object to specify the set
     * of changes that needs to be made.  Changes are specified in terms
     * of attribute values.  Each attribute value to be modified, added,
     * or removed must be specified by an <CODE>LDAPAttribute</CODE> object.
     * <P>
     *
     * For example, the following section of code changes Barbara Jensen's
     * title, adds a telephone number to the entry, and removes the room
     * number from the entry. <P>
     *
     * <PRE>
     * ...
     * String myEntryDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     *
     * LDAPModificationSet manyChanges = new LDAPModificationSet();
     * LDAPAttribute attrTelephoneNumber = new LDAPAttribute( "telephoneNumber",
     *                                                        "555-1212" );
     * manyChanges.add( LDAPModification.ADD, attrTelephoneNumber );
     * LDAPAttribute attrRoomNumber = new LDAPAttribute( "roomnumber", "222" );
     * manyChanges.add( LDAPModification.DELETE, attrRoomNumber );
     * LDAPAttribute attrTitle = new LDAPAttribute( "title",
     *                                       "Manager of Product Development" );
     * manyChanges.add( LDAPModification.REPLACE, attrTitle );
     *
     * myConn.modify( myEntryDN, manyChanges );
     * ... </PRE>
     *
     * @param DN The distinguished name of the entry that you want to modify
     * @param mods A set of changes to be made to the entry
     * @exception LDAPException Failed to make the specified changes to the
     * directory entry.
     * @see netscape.ldap.LDAPModificationSet
     */
    public void modify (String DN, LDAPModificationSet mods)
        throws LDAPException {
        modify(DN, mods, defaultConstraints);
    }

    /**
     * Makes a set of changes to an existing entry in the directory and
     * allows you to specify preferences for this LDAP modify operation
     * by using an <CODE>LDAPSearchConstraints</CODE> object. For
     * example, you can specify whether or not to follow referrals.
     * You can also apply LDAP v3 controls to the operation.
     * <P>
     *
     * @param DN The distinguished name of the entry that you want to modify
     * @param mods A set of changes to be made to the entry
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to make the specified changes to the
     * directory entry.
     * @see netscape.ldap.LDAPModificationSet
     * @see netscape.ldap.LDAPSearchConstraints
     */
     public void modify (String DN, LDAPModificationSet mods,
         LDAPSearchConstraints cons) throws LDAPException {
         LDAPModification[] modList = new LDAPModification[mods.size()];
         for( int i = 0; i < mods.size(); i++ )
             modList[i] = mods.elementAt( i );
         modify (DN, modList, cons);
     }

    /**
     * Makes a set of changes to an existing entry in the directory
     * (for example, changes attribute values, adds new attribute values,
     * or removes existing attribute values). <P>
     *
     * Use an array of <CODE>LDAPModification</CODE> objects to specify the
     * changes that need to be made.  Each change must be specified by
     * an <CODE>LDAPModification</CODE> object, and each attribute value
     * to be modified, added, or removed must be specified by an
     * <CODE>LDAPAttribute</CODE> object. <P>
     *
     * @param DN The distinguished name of the entry that you want to modify
     * @param mods An array of objects representing the changes to be made
     * to the entry
     * @exception LDAPException Failed to make the specified changes to the
     * directory entry.
     * @see netscape.ldap.LDAPModification
     */
    public void modify (String DN, LDAPModification[] mods)
        throws LDAPException {
        modify(DN, mods, defaultConstraints);
    }

    /**
     * Makes a set of changes to an existing entry in the directory and
     * allows you to specify preferences for this LDAP modify operation
     * by using an <CODE>LDAPSearchConstraints</CODE> object. For
     * example, you can specify whether or not to follow referrals.
     * You can also apply LDAP v3 controls to the operation.
     * <P>
     *
     * @param DN The distinguished name of the entry that you want to modify
     * @param mods An array of objects representing the changes to be made
     * to the entry
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to make the specified changes to the
     * directory entry.
     * @see netscape.ldap.LDAPModification
     * @see netscape.ldap.LDAPSearchConstraints
     */
    public void modify (String DN, LDAPModification[] mods,
         LDAPSearchConstraints cons) throws LDAPException {
         bind (cons);

         LDAPResponseListener myListener = getResponseListener ();
         JDAPMessage response = null;
         try {
             sendRequest (new JDAPModifyRequest (DN, mods), myListener, cons);
             response = myListener.getResponse();
             checkMsg (response);
         } catch (LDAPReferralException e) {
             performReferrals(e, cons, JDAPProtocolOp.MODIFY_REQUEST,
               DN, 0, null, null, false, mods, null, null, null);
         } finally {
             releaseResponseListener (myListener);
         }
    }

    /**
     * Deletes the entry for the specified DN from the directory. <P>
     *
     * For example, the following section of code deletes the entry for
     * Barbara Jensen from the directory. <P>
     *
     * <PRE>
     * ...
     * String myEntryDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * myConn.delete( myEntryDN );
     * ... </PRE>
     *
     * @param DN Distinguished name identifying the entry that you want
     * to remove from the directory.
     * @exception LDAPException Failed to delete the specified entry from
     * the directory.
     */
    public void delete( String DN ) throws LDAPException {
        delete(DN, defaultConstraints);
    }

    /**
     * Deletes the entry for the specified DN from the directory and
     * allows you to specify preferences for this LDAP delete operation
     * by using an <CODE>LDAPSearchConstraints</CODE> object. For
     * example, you can specify whether or not to follow referrals.
     * You can also apply LDAP v3 controls to the operation.
     * <P>
     *
     * @param DN Distinguished name identifying the entry that you want
     * to remove from the directory.
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to delete the specified entry from
     * the directory.
     * @see netscape.ldap.LDAPSearchConstraints
     */
    public void delete( String DN, LDAPSearchConstraints cons )
        throws LDAPException {
        bind (cons);

        LDAPResponseListener myListener = getResponseListener ();
        JDAPMessage response;
        try {
            sendRequest (new JDAPDeleteRequest (DN), myListener, cons);
            response = myListener.getResponse();
            checkMsg (response);
        } catch (LDAPReferralException e) {
            performReferrals(e, cons, JDAPProtocolOp.DEL_REQUEST,
              DN, 0, null, null, false, null, null, null, null);
        } finally {
            releaseResponseListener (myListener);
        }
    }

    /**
     * Renames an existing entry in the directory. <P>
     *
     * You can specify whether or not the original name of the entry is
     * retained as a value in the entry. For example, suppose you rename
     * the entry "cn=Barbara" to "cn=Babs".  You can keep "cn=Barbara"
     * as a value in the entry so that the cn attribute has two values: <P>
     *
     * <PRE>
     *       cn=Barbara
     *       cn=Babs
     * </PRE>
     * The following example renames an entry.  The old name of the entry
     * is kept as a value in the entry. <P>
     *
     * <PRE>
     * ...
     * String myEntryDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * String newRDN = "cn=Babs Jensen";
     * myConn.rename( myEntryDN, newRDN, false );
     * ... </PRE>
     *
     * @param DN Current distinguished name of the entry.
     * @param newRDN New relative distinguished name for the entry (for example,
     * "cn=newName").
     * @param deleteOldRDN If <CODE>true</CODE>, the old name is not retained
     * as an attribute value (for example, the attribute value "cn=oldName" is
     * removed).  If <CODE>false</CODE>, the old name is retained
     * as an attribute value (for example, the entry might now have two values
     * for the cn attribute: "cn=oldName" and "cn=newName").
     * @exception LDAPException Failed to rename the specified entry.
     */
    public void rename (String DN, String newRDN, boolean deleteOldRDN )
        throws LDAPException {
        rename(DN, newRDN, null, deleteOldRDN);
    }

    /**
     * Renames an existing entry in the directory. <P>
     *
     * You can specify whether or not the original name of the entry is
     * retained as a value in the entry. For example, suppose you rename
     * the entry "cn=Barbara" to "cn=Babs".  You can keep "cn=Barbara"
     * as a value in the entry so that the cn attribute has two values: <P>
     *
     * <PRE>
     *       cn=Barbara
     *       cn=Babs
     * </PRE>
     * The following example renames an entry.  The old name of the entry
     * is kept as a value in the entry. <P>
     *
     * <PRE>
     * ...
     * String myEntryDN = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US";
     * String newRDN = "cn=Babs Jensen";
     * myConn.rename( myEntryDN, newRDN, false );
     * ... </PRE>
     *
     * @param DN Current distinguished name of the entry.
     * @param newRDN New relative distinguished name for the entry (for example,
     * "cn=newName").
     * @param deleteOldRDN If <CODE>true</CODE>, the old name is not retained
     * as an attribute value (for example, the attribute value "cn=oldName" is
     * removed).  If <CODE>false</CODE>, the old name is retained
     * as an attribute value (for example, the entry might now have two values
     * for the cn attribute: "cn=oldName" and "cn=newName").
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to rename the specified entry.
     */
    public void rename (String DN, String newRDN, boolean deleteOldRDN,
        LDAPSearchConstraints cons )
          throws LDAPException {
        rename(DN, newRDN, null, deleteOldRDN, cons);
    }

    /**
     * Renames an existing entry in the directory and (optionally)
     * changes the location of the entry in the directory tree.<P>
     *
     * <B>NOTE: </B>Netscape Directory Server 3.0 does not support the
     * capability to move an entry to a different location in the
     * directory tree.  If you specify a value for the <CODE>newParentDN</CODE>
     * argument, an <CODE>LDAPException</CODE> will be thrown.
     * <P>
     *
     * @param DN Current distinguished name of the entry.
     * @param newRDN New relative distinguished name for the entry (for example,
     * "cn=newName").
     * @param newParentDN If not null, the distinguished name for the
     * entry under which the entry should be moved (for example, to move
     * an entry under the Accounting subtree, specify this argument as
     * "ou=Accounting, o=Ace Industry, c=US").
     * @param deleteOldRDN If <CODE>true</CODE>, the old name is not retained
     * as an attribute value (for example, the attribute value "cn=oldName" is
     * removed).  If <CODE>false</CODE>, the old name is retained
     * as an attribute value (for example, the entry might now have two values
     * for the cn attribute: "cn=oldName" and "cn=newName").
     * @exception LDAPException Failed to rename the specified entry.
     */
     public void rename(String dn,
                        String newRDN,
                        String newParentDN,
                        boolean deleteOldRDN) throws LDAPException {
          rename(dn, newRDN, newParentDN, deleteOldRDN, defaultConstraints);
     }

    /**
     * Renames an existing entry in the directory and (optionally)
     * changes the location of the entry in the directory tree. Also
     * allows you to specify preferences for this LDAP modify DN operation
     * by using an <CODE>LDAPSearchConstraints</CODE> object. For
     * example, you can specify whether or not to follow referrals.
     * You can also apply LDAP v3 controls to the operation.
     * <P>
     *
     * <B>NOTE: </B>Netscape Directory Server 3.0 does not support the
     * capability to move an entry to a different location in the
     * directory tree.  If you specify a value for the <CODE>newParentDN</CODE>
     * argument, an <CODE>LDAPException</CODE> will be thrown.
     * <P>
     *
     * @param DN Current distinguished name of the entry.
     * @param newRDN New relative distinguished name for the entry (for example,
     * "cn=newName").
     * @param newParentDN If not null, the distinguished name for the
     * entry under which the entry should be moved (for example, to move
     * an entry under the Accounting subtree, specify this argument as
     * "ou=Accounting, o=Ace Industry, c=US").
     * @param deleteOldRDN If <CODE>true</CODE>, the old name is not retained
     * as an attribute value (for example, the attribute value "cn=oldName" is
     * removed).  If <CODE>false</CODE>, the old name is retained
     * as an attribute value (for example, the entry might now have two values
     * for the cn attribute: "cn=oldName" and "cn=newName").
     * @param cons The set of preferences that you want applied to this operation.
     * @exception LDAPException Failed to rename the specified entry.
     * @see netscape.ldap.LDAPSearchConstraints
     */
    public void rename (String DN,
                           String newRDN,
                           String newParentDN,
                           boolean deleteOldRDN,
                           LDAPSearchConstraints cons)
        throws LDAPException {
        bind (cons);

        LDAPResponseListener myListener = getResponseListener ();
        JDAPMessage response;
        try {
            JDAPModifyRDNRequest request = null;
            if ( newParentDN != null )
                request = new JDAPModifyRDNRequest (DN,
                                                newRDN,
                                                deleteOldRDN,
                                                newParentDN);
            else
                request = new JDAPModifyRDNRequest (DN,
                                                newRDN,
                                                deleteOldRDN);
            sendRequest (request, myListener, cons);
            response = myListener.getResponse();
            checkMsg (response);
        } catch (LDAPReferralException e) {
            performReferrals(e, cons, JDAPProtocolOp.MODIFY_RDN_REQUEST,
              DN, 0, newRDN, null, deleteOldRDN, null, null, null, null);
        } finally {
            releaseResponseListener (myListener);
        }
    }

    /**
     * Returns the value of the specified option for this
     * <CODE>LDAPConnection</CODE> object. <P>
     *
     * These options represent the search constraints for the current connection.
     * To get all search constraints for the current connection, call the
     * <CODE>getSearchConstraints</CODE> method.
     * <P>
     *
     * By default, the search constraints apply to all searches performed
     * through the current connection.  You can change these search constraints:
     * <P>
     *
     * <UL>
     * <LI>If you want to override these constraints only for a particular
     * search, create an <CODE>LDAPSearchConstraints</CODE> object with
     * your new constraints and pass it to the
     * <CODE>LDAPConnection.search</CODE> method.
     * <P>
     *
     * <LI>If you want to override these constraints for all searches
     * performed under the current connection, call the
     * <CODE>setOption</CODE> method to change the search constraint.
     * <P>
     *
     * </UL>
     * <P>
     *
     * For example, the following section of code gets and prints the
     * maximum number of search results that are returned for searches
     * performed through this connection.  (This applies to all searches
     * unless a different set of search constraints is specified in an
     * <CODE>LDAPSearchConstraints</CODE> object.)
     * <P>
     *
     * <PRE>
     * LDAPConnection ld = new LDAPConnection();
     * int sizeLimit = ( (Integer)ld.getOption( LDAPv2.SIZELIMIT ) ).intValue();
     * System.out.println( "Maximum number of results: " + sizeLimit );
     * </PRE>
     *
     * @param option You can specify one of the following options:
     * <TABLE CELLPADDING=5>
     * <TR VALIGN=BASELINE ALIGN=LEFT>
     * <TH>Option</TH><TH>Data Type</TH><TH>Description</TH></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.PROTOCOL_VERSION</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the version of the LDAP protocol used by the
     * client.
     * <P>By default, the value of this option is 2.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.DEREF</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies when your client dereferences aliases.
     *<PRE>
     * Legal values for this option are:
     *
     * DEREF_NEVER       Aliases are never dereferenced.
     *
     * DEREF_FINDING     Aliases are dereferenced when find-
     *                   ing the starting point for the
     *                   search (but not when searching
     *                   under that starting entry).
     *
     * DEREF_SEARCHING   Aliases are dereferenced when
     *                   searching the entries beneath the
     *                   starting point of the search (but
     *                   not when finding the starting
     *                   entry).
     *
     * DEREF_ALWAYS      Aliases are always dereferenced.
     *</PRE>
     * <P>By default, the value of this option is
     * <CODE>DEREF_NEVER</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.SIZELIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of search results to return.
     * If this option is set to 0, there is no maximum limit.
     * <P>By default, the value of this option is 1000.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.TIMELIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of milliseconds to wait for results
     * before timing out. If this option is set to 0, there is no maximum
     * time limit.
     * <P>By default, the value of this option is 0.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS</CODE></TD>
     * <TD><CODE>Boolean</CODE></TD>
     * <TD>Specifies whether or not your client follows referrals automatically.
     * If <CODE>true</CODE>, your client follows referrals automatically.
     * If <CODE>false</CODE>, an <CODE>LDAPReferralException</CODE> is raised
     * when referral is detected.
     * <P>By default, the value of this option is <CODE>false</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS_REBIND_PROC</CODE></TD>
     * <TD><CODE>LDAPRebind</CODE></TD>
     * <TD>Specifies an object with a class that implements the
     * <CODE>LDAPRebind</CODE> interface.  You must define this class and
     * the <CODE>getRebindAuthentication</CODE> method that will be used to
     * get the distinguished name and password to use for authentication.
     * <P>By default, the value of this option is <CODE>null</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS_HOP_LIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of referrals in a sequence that
     * your client will follow.  (For example, if REFERRALS_HOP_LIMIT is 5,
     * your client will follow no more than 5 referrals in a row when resolving
     * a single LDAP request.)
     * <P>By default, the value of this option is 10.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.BATCHSIZE</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the number of search results to return at a time.
     * (For example, if BATCHSIZE is 1, results are returned one at a time.)
     * <P>By default, the value of this option is 1.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv3.CLIENTCONTROLS</CODE></TD>
     * <TD><CODE>LDAPControl[]</CODE></TD>
     * <TD>Specifies the client controls that may affect the handling of LDAP
     * operations in the LDAP classes. These controls are used by the client
     * and are not passed to the LDAP server. At this time, no client controls
     * are defined for clients built with the Netscape LDAP classes. </TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv3.SERVERCONTROLS</CODE></TD>
     * <TD><CODE>LDAPControl[]</CODE></TD>
     * <TD>Specifies the server controls that are passed to the LDAP
     * server on each LDAP operation. Not all servers support server
     * controls; a particular server may or may not support a given
     * server control.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>MAXBACKLOG</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of search results to accumulate in an
     * LDAPSearchResults before suspending the reading of input from the server.
     * <P>By default, the value of this option is 100.</TD></TR>
     * </TABLE><P>
     * @return The value for the option wrapped in an object.  (You
     * need to cast the returned value as its appropriate type. For
     * example, when getting the SIZELIMIT option, cast the returned
     * value as an <CODE>Integer</CODE>.)
     * @exception LDAPException Failed to get the specified option.
     * @see netscape.ldap.LDAPRebind
     * @see netscape.ldap.LDAPSearchConstraints
     * @see netscape.ldap.LDAPReferralException
     * @see netscape.ldap.LDAPControl
     * @see netscape.ldap.LDAPConnection#getSearchConstraints
     * @see netscape.ldap.LDAPConnection#search(java.lang.String, int, java.lang.String, java.lang.String[], boolean, netscape.ldap.LDAPSearchConstraints)
     */
    public Object getOption( int option ) throws LDAPException {
        if (option == LDAPv2.PROTOCOL_VERSION) {
            return new Integer(protocolVersion);
        }

        return getOption(option, defaultConstraints);
    }

    private static Object getOption( int option, LDAPSearchConstraints cons )
        throws LDAPException {
        switch (option) {
            case LDAPv2.DEREF:
              return new Integer (cons.getDereference());
            case LDAPv2.SIZELIMIT:
              return new Integer (cons.getMaxResults());
            case LDAPv2.TIMELIMIT:
              return new Integer (cons.getTimeLimit());
            case LDAPv2.REFERRALS:
              return new Boolean (cons.getReferrals());
            case LDAPv2.REFERRALS_REBIND_PROC:
              return cons.getRebindProc();
            case LDAPv2.REFERRALS_HOP_LIMIT:
              return new Integer (cons.getHopLimit());
            case LDAPv2.BATCHSIZE:
              return new Integer (cons.getBatchSize());
            case LDAPv3.CLIENTCONTROLS:
              return cons.getClientControls();
            case LDAPv3.SERVERCONTROLS:
              return cons.getServerControls();
            case MAXBACKLOG:
              return new Integer (cons.getMaxBacklog());
            default:
              throw new LDAPException ( "invalid option",
                                        LDAPException.PARAM_ERROR );
        }
    }

    /**
     * Sets the value of the specified option for this
     * <CODE>LDAPConnection</CODE> object. <P>
     *
     * These options represent the search constraints for the current
     * connection.
     * To get all search constraints for the current connection, call the
     * <CODE>getSearchConstraints</CODE> method.
     * <P>
     *
     * By default, the option that you set applies to all subsequent
     * searches performed through the current connection. If you want to
     * set a constraint only for a particular search, create an
     * <CODE>LDAPSearchConstraints</CODE> object with your new constraints
     * and pass it to the <CODE>LDAPConnection.search</CODE> method.
     * <P>
     *
     * For example, the following section of code changes the constraint for
     * the maximum number of search results that are returned for searches
     * performed through this connection.  (This applies to all searches
     * unless a different set of search constraints is specified in an
     * <CODE>LDAPSearchConstraints</CODE> object.)
     * <P>
     *
     * <PRE>
     * LDAPConnection ld = new LDAPConnection();
     * Integer newLimit = new Integer( 20 );
     * ld.setOption( LDAPv2.SIZELIMIT, newLimit );
     * System.out.println( "Changed the maximum number of results to " + newLimit.intValue() );
     * </PRE>
     *
     * @param option You can specify one of the following options:
     * <TABLE CELLPADDING=5>
     * <TR VALIGN=BASELINE ALIGN=LEFT>
     * <TH>Option</TH><TH>Data Type</TH><TH>Description</TH></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.PROTOCOL_VERSION</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the version of the LDAP protocol used by the
     * client.
     * <P>By default, the value of this option is 2.  If you want
     * to use LDAP v3 features (such as extended operations or
     * controls), you need to set this value to 3. </TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.DEREF</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies when your client dereferences aliases.
     *<PRE>
     * Legal values for this option are:
     *
     * DEREF_NEVER       Aliases are never dereferenced.
     *
     * DEREF_FINDING     Aliases are dereferenced when find-
     *                   ing the starting point for the
     *                   search (but not when searching
     *                   under that starting entry).
     *
     * DEREF_SEARCHING   Aliases are dereferenced when
     *                   searching the entries beneath the
     *                   starting point of the search (but
     *                   not when finding the starting
     *                   entry).
     *
     * DEREF_ALWAYS      Aliases are always dereferenced.
     *</PRE>
     * <P>By default, the value of this option is
     * <CODE>DEREF_NEVER</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.SIZELIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of search results to return.
     * If this option is set to 0, there is no maximum limit.
     * <P>By default, the value of this option is 1000.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.TIMELIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of milliseconds to wait for results
     * before timing out. If this option is set to 0, there is no maximum
     * time limit.
     * <P>By default, the value of this option is 0.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS</CODE></TD>
     * <TD><CODE>Boolean</CODE></TD>
     * <TD>Specifies whether or not your client follows referrals automatically.
     * If <CODE>true</CODE>, your client follows referrals automatically.
     * If <CODE>false</CODE>, an <CODE>LDAPReferralException</CODE> is
     * raised when a referral is detected.
     * <P>By default, the value of this option is <CODE>false</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS_REBIND_PROC</CODE></TD>
     * <TD><CODE>LDAPRebind</CODE></TD>
     * <TD>Specifies an object with a class that implements the
     * <CODE>LDAPRebind</CODE>
     * interface.  You must define this class and the
     * <CODE>getRebindAuthentication</CODE> method that will be used to get
     * the distinguished name and password to use for authentication.
     * <P>By default, the value of this option is <CODE>null</CODE>.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.REFERRALS_HOP_LIMIT</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of referrals in a sequence that
     * your client will follow.  (For example, if REFERRALS_HOP_LIMIT is 5,
     * your client will follow no more than 5 referrals in a row when resolving
     * a single LDAP request.)
     * <P>By default, the value of this option is 10.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv2.BATCHSIZE</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the number of search results to return at a time.
     * (For example, if BATCHSIZE is 1, results are returned one at a time.)
     * <P>By default, the value of this option is 1.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv3.CLIENTCONTROLS</CODE></TD>
     * <TD><CODE>LDAPControl[]</CODE></TD>
     * <TD>Specifies the client controls that may affect handling of LDAP
     * operations in the LDAP classes. These controls are used by the client
     * and are not passed to the server. At this time, no client controls
     * are defined for clients built with the Netscape LDAP classes. </TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>LDAPv3.SERVERCONTROLS</CODE></TD>
     * <TD><CODE>LDAPControl[]</CODE></TD>
     * <TD>Specifies the server controls that are passed to the LDAP
     * server on each LDAP operation. Not all servers support server
     * controls; a particular server may or may not support a particular
     * control.</TD></TR>
     * <TR VALIGN=BASELINE><TD>
     * <CODE>MAXBACKLOG</CODE></TD>
     * <TD><CODE>Integer</CODE></TD>
     * <TD>Specifies the maximum number of search results to accumulate in an
     * LDAPSearchResults before suspending the reading of input from the server.
     * <P>By default, the value of this option is 100.</TD></TR>
     * </TABLE><P>
     * @param value The value to assign to the option.  The value must be
     * the java.lang object wrapper for the appropriate parameter
     * (e.g. boolean->Boolean,
     *   integer->Integer)
     * @exception LDAPException Failed to set the specified option.
     * @see netscape.ldap.LDAPRebind
     * @see netscape.ldap.LDAPSearchConstraints
     * @see netscape.ldap.LDAPReferralException
     * @see netscape.ldap.LDAPControl
     * @see netscape.ldap.LDAPConnection#getSearchConstraints
     * @see netscape.ldap.LDAPConnection#search(java.lang.String, int, java.lang.String, java.lang.String[], boolean, netscape.ldap.LDAPSearchConstraints)
     */
    public void setOption( int option, Object value ) throws LDAPException {
        if (option == LDAPv2.PROTOCOL_VERSION) {
            setProtocolVersion(((Integer)value).intValue());
            return;
        }
        setOption(option, value, defaultConstraints);
    }

    private static void setOption( int option, Object value, LDAPSearchConstraints cons ) throws LDAPException {
      try {
        switch (option) {
        case LDAPv2.DEREF:
          cons.setDereference(((Integer)value).intValue());
          return;
        case LDAPv2.SIZELIMIT:
          cons.setMaxResults(((Integer)value).intValue());
          return;
        case LDAPv2.TIMELIMIT:
          cons.setTimeLimit(((Integer)value).intValue());
          return;
        case LDAPv2.REFERRALS:
          cons.setReferrals(((Boolean)value).booleanValue());
          return;
        case LDAPv2.REFERRALS_REBIND_PROC:
          cons.setRebindProc((LDAPRebind)value);
          return;
        case LDAPv2.REFERRALS_HOP_LIMIT:
          cons.setHopLimit(((Integer)value).intValue());
          return;
        case LDAPv2.BATCHSIZE:
          cons.setBatchSize(((Integer)value).intValue());
          return;
        case LDAPv3.CLIENTCONTROLS:
          if ( value == null )
            cons.setClientControls( (LDAPControl[]) null );
          else if ( value instanceof LDAPControl )
            cons.setClientControls( (LDAPControl) value );
          else if ( value instanceof LDAPControl[] )
            cons.setClientControls( (LDAPControl[])value );
          else
            throw new LDAPException ( "invalid LDAPControl",
                                      LDAPException.PARAM_ERROR );
          return;
        case LDAPv3.SERVERCONTROLS:
          if ( value == null )
            cons.setServerControls( (LDAPControl[]) null );
          else if ( value instanceof LDAPControl )
            cons.setServerControls( (LDAPControl) value );
          else if ( value instanceof LDAPControl[] )
            cons.setServerControls( (LDAPControl[])value );
          else
            throw new LDAPException ( "invalid LDAPControl",
                                      LDAPException.PARAM_ERROR );
          return;
        case MAXBACKLOG:
            int val = ((Integer)value).intValue();
            if ( val < 1 ) {
                throw new LDAPException ( "MAXBACKLOG must be at least 1",
                                          LDAPException.PARAM_ERROR );
            } else {
                cons.setMaxBacklog(((Integer)value).intValue());
            }
            return;
        default:
          throw new LDAPException ("invalid option",
                                   LDAPException.PARAM_ERROR );
        }
      } catch (ClassCastException cc) {
        throw new LDAPException ("invalid option value",
                                 LDAPException.PARAM_ERROR );
      }
    }

    /**
     * Returns an array of the latest controls (if any) from server.
     * @return An array of the controls returned by an operation, or
     * null if none.
     * @see netscape.ldap.LDAPControl
     */
    public LDAPControl[] getResponseControls() {
      LDAPControl[] controls = null;

      /* Get the latest controls returned for our thread */
      synchronized(m_responseControlTable) {
          Vector responses = (Vector)m_responseControlTable.get(th);

          if (responses != null) {
              // iterate through each response control
              for (int i=0,size=responses.size(); i<size; i++) {
                  ResponseControl responseObj =
                    (ResponseControl)responses.elementAt(i);

                  // check if the response belongs to this connection
                  if (responseObj.getConnection().equals(this)) {
                      controls = responseObj.getControls();
                      responses.removeElementAt(i);
                      break;
                  }
              }
          }
      }

      return controls;
    }

    /**
     * Returns the set of search constraints that apply to all searches
     * performed through this connection (unless you specify a different
     * set of search constraints when calling the <CODE>search</CODE>
     * method). <P>
     *
     * Note that if you want to get individual constraints (rather than
     * getting the
     * entire set of constraints), call the <CODE>getOption</CODE> method.
     * <P>
     *
     * Typically, you might call the <CODE>getSearchConstraints</CODE> method
     * if you want to create a slightly different set of search constraints
     * that you want to apply to a particular search.
     * <P>
     *
     * For example, the following section of code changes the maximum number
     * of results to 10 for a specific search. Rather than construct a new
     * set of search constraints from scratch, the example gets the current
     * settings for the connections and just changes the setting for the
     * maximum results.
     * <P>
     *
     * Note that this change only applies to the searches performed with this
     * custom set of constraints.  All other searches performed through this
     * connection use the original set of search constraints.
     * <P>
     *
     * <PRE>
     * ...
     * LDAPSearchConstraints myOptions = ld.getSearchConstraints();
     * myOptions.setMaxResults( 10 );
     * String[] myAttrs = { "objectclass" };
     * LDAPSearchResults myResults = ld.search( "o=Ace Industry,c=US",
     *                                          LDAPv2.SCOPE_SUB,
     *                                          "(objectclass=*)",
     *                                          myAttrs,
     *                                          false,
     *                                          myOptions );
     * ...
     * </PRE>
     *
     * @return The <CODE>LDAPSearchConstraints</CODE> object representing the
     * set of search constraints that apply (by default) to all searches
     * performed through this connection.
     * @see netscape.ldap.LDAPSearchConstraints
     * @see netscape.ldap.LDAPConnection#getOption
     * @see netscape.ldap.LDAPConnection#search(java.lang.String, int, java.lang.String, java.lang.String[], boolean, netscape.ldap.LDAPSearchConstraints)
     */
    public LDAPSearchConstraints getSearchConstraints () {
        return defaultConstraints;
    }

    /**
     * Get a new listening agent from the internal buffer of available agents.
     * These objects are used to make the asynchronous LDAP operations
     * synchronous.
     * @return response listener object
     */
    private synchronized LDAPResponseListener getResponseListener () {
        if (responseListeners == null)
            responseListeners = new Vector (5);

        LDAPResponseListener l;
        if ( responseListeners.size() < 1 ) {
            l = new LDAPResponseListener ( this );
        }
        else {
            l = (LDAPResponseListener)responseListeners.elementAt (0);
            responseListeners.removeElementAt (0);
        }
        return l;
    }

    /**
     * Get a new search listening agent from the internal buffer of available
     * agents. These objects are used to make the asynchronous LDAP operations
     * synchronous.
     * @return A search response listener object
     */
    private synchronized LDAPSearchListener getSearchListener (
        LDAPSearchConstraints cons )
    {
        if (searchListeners == null) {
            searchListeners = new Vector (5);
        }

        LDAPSearchListener l;
        if ( searchListeners.size() < 1 ) {
            l = new LDAPSearchListener ( this, cons );
        }
        else {
            l = (LDAPSearchListener)searchListeners.elementAt (0);
            searchListeners.removeElementAt (0);
        }
        return l;
    }

    /**
     * Put a listening agent into the internal buffer of available agents.
     * These objects are used to make the asynchronous LDAP operations
     * synchronous.
     * @param l Listener to buffer
     */
    private synchronized void releaseResponseListener (LDAPResponseListener l)
    {
        if (responseListeners == null)
            responseListeners = new Vector (5);

        l.reset ();
        responseListeners.addElement (l);
    }

    /**
     * Put a search listening agent into the internal buffer of available
     * agents. These objects are used to make the asynchronous LDAP
     * operations synchronous.
     * @param Listener to buffer
     */
    synchronized void releaseSearchListener (LDAPSearchListener l)
    {
        if (searchListeners == null)
            searchListeners = new Vector (5);

        l.reset ();
        searchListeners.addElement (l);
    }

    /**
     * Checks the message (assumed to be a return value).  If the resultCode
     * is anything other than SUCCESS, it throws an LDAPException describing
     * the server's (error) response.
     * @param m Server response to validate
     * @exception LDAPException failed to check message
     */
    void checkMsg (JDAPMessage m) throws LDAPException {
      if (m.getProtocolOp() instanceof JDAPResult) {
          JDAPResult response = (JDAPResult)(m.getProtocolOp());
          int resultCode = response.getResultCode ();

          if (resultCode == JDAPResult.SUCCESS)
              return;

          if (resultCode == JDAPResult.REFERRAL)
              throw new LDAPReferralException ("referral", resultCode,
                response.getReferrals());

          if (resultCode == JDAPResult.LDAP_PARTIAL_RESULTS)
              throw new LDAPReferralException ("referral", resultCode,
                response.getErrorMessage());
          else
              throw new LDAPException ("error result", resultCode,
                response.getErrorMessage(),
                response.getMatchedDN());

      } else if (m.getProtocolOp() instanceof JDAPSearchResultReference) {
          String[] referrals =
            ((JDAPSearchResultReference)m.getProtocolOp()).getUrls();
          throw new LDAPReferralException ("referral",
            JDAPResult.SUCCESS, referrals);
      } else
          return;
    }

    /**
     * Set response controls for the current connection for a particular 
     * thread. Get the oldest returned controls and remove them from the 
     * queue. If the connection is executing a persistent search, there may
     * be more than one set of controls in the queue. For any other 
     * operation, there will only ever be at most one set of controls 
     * (controls from any earlier operation are replaced by controls 
     * received on the latest operation on this connection by this thread).
     * @param current The target thread.
     * @param con The server response controls.
     */
    void setResponseControls( LDAPConnThread current, ResponseControl con ) {
        synchronized(m_responseControlTable) {
            Vector v = (Vector)m_responseControlTable.get(current);

            // if the current thread already contains response controls from
            // a previous operation
            if ((v != null) && (v.size() > 0)) {

                // look at each response control
                for (int i=v.size()-1; i>=0; i--) {
                    ResponseControl response = 
                      (ResponseControl)v.elementAt(i);
    
                    // if this response control belongs to this connection
                    if (response.getConnection().equals(this)) {
 
                        // if the given control is null or 
                        // the given control is not null and the current 
                        // control does not correspond to the new JDAPMessage
                        if ((con == null) || 
                          (con.getMsgID() != response.getMsgID()))
                            v.removeElement(response);

                        // For the same connection, if the message id from the 
                        // given control is the same as the one in the queue,
                        // those controls in the queue will not get removed
                        // since they come from the persistent search control
                        // which allows more than one control response for
                        // one persistent search operation.
                    }
                }
            } else {
                if (con != null)
                    v = new Vector();
            }          

            if (con != null) {
                v.addElement(con);
                m_responseControlTable.put(current, v);
            }

            /* Do some garbage collection: check if any attached threads have
               exited */
            /* Now check all threads in the list */
            Enumeration e = m_attachedList.elements();
            while( e.hasMoreElements() ) {
                LDAPConnThread aThread = (LDAPConnThread)e.nextElement();
                if ( !aThread.isAlive() ) {
                    m_responseControlTable.remove( aThread );
                    m_attachedList.removeElement( aThread );
                }
            }
        }
        /* Make sure we're registered */
        if ( m_attachedList.indexOf( current ) < 0 )
            m_attachedList.addElement( current );
    }

    /**
     * Set up connection for referral.
     * @param u referral URL
     * @param cons search constraints
     * @return new LDAPConnection, already connected and authenticated
     */
    private LDAPConnection prepareReferral( LDAPUrl u,
                                            LDAPSearchConstraints cons )
        throws LDAPException {
        LDAPConnection connection = new LDAPConnection (this.getSocketFactory());
        connection.setOption(REFERRALS, new Boolean(true));
        connection.setOption(REFERRALS_REBIND_PROC,
                              cons.getRebindProc());

        // need to set the protocol version which gets passed to connection
        connection.setOption(PROTOCOL_VERSION,
                              new Integer(protocolVersion));

        connection.setOption(REFERRALS_HOP_LIMIT,
                              new Integer(cons.getHopLimit()-1));
        connection.connect (u.getHost(), u.getPort());
        return connection;
    }

    /**
     * Establish the LDAPConnection to the referred server. This one is used
     * for bind operation only since we need to keep this new connection for
     * the subsequent operations. This new connection will be destroyed in
     * two circumstances: disconnect gets called or the client binds as
     * someone else.
     * @return The new LDAPConnection to the referred server
     */
    LDAPConnection createReferralConnection(LDAPReferralException e,
      LDAPSearchConstraints cons) throws LDAPException {
        if (cons.getHopLimit() <= 0)
            throw new LDAPException("exceed hop limit",
              e.getLDAPResultCode(), e.getLDAPErrorMessage());
        if (!cons.getReferrals()) {
            throw e;
        }

        LDAPUrl u[] = e.getURLs();
        // If there are no referrals (because the server isn't set up for
        // them), give up here
        if ((u == null) || (u.length <= 0) || (u[0].equals("")))
            throw new LDAPException("No target URL in referral",
              LDAPException.NO_RESULTS_RETURNED);

        LDAPConnection connection = null;
        connection = prepareReferral(u[0], cons);
        String DN = u[0].getDN();
        if ((DN == null) || (DN.equals("")))
            DN = boundDN;
        connection.authenticate(protocolVersion, DN, boundPasswd);
        return connection;
    }

    /**
     * Performs the referral.
     * @param e referral exception
     * @param cons search constraints
     */
    void performReferrals(LDAPReferralException e,
        LDAPSearchConstraints cons, int ops,
        /* unions of different operation parameters */
        String dn, int scope, String filter, String types[],
        boolean attrsOnly, LDAPModification mods[], LDAPEntry entry,
        LDAPAttribute attr,
        /* result */
        Vector results
        ) throws LDAPException {

        if (cons.getHopLimit() <= 0)
            throw new LDAPException("exceed hop limit",
              e.getLDAPResultCode(), e.getLDAPErrorMessage());
        if (!cons.getReferrals()) {
            if (ops == JDAPProtocolOp.SEARCH_REQUEST) {
                LDAPSearchResults res = new LDAPSearchResults();
                res.add(e);
                results.addElement(res);
                return;
            } else
                throw e;
        }

        LDAPUrl u[] = e.getURLs();
        // If there are no referrals (because the server isn't set up for
        // them), give up here
        if ( u == null )
            return;

        for (int i = 0; i < u.length; i++) {
            String newDN = u[i].getDN();
            String DN = null;
            if ((newDN == null) || (newDN.equals("")))
                DN = dn;
            else
                DN = newDN;

            LDAPSearchResults res = null;
            LDAPSearchConstraints newcons = (LDAPSearchConstraints)cons.clone();
            newcons.setHopLimit( cons.getHopLimit()-1 );

            try {
                if ((m_referralConnection != null) && 
                  (u[i].getHost().equals(m_referralConnection.host)) &&
                  (u[i].getPort() == m_referralConnection.port)) {
                    performReferrals(m_referralConnection, newcons, ops, DN, 
                      scope, filter, types, attrsOnly, mods, entry, attr, 
                      results);
                    continue;
                }
            } catch (LDAPException le) {
                if (le.getLDAPResultCode() != 
                  LDAPException.INSUFFICIENT_ACCESS_RIGHTS) {
                    throw le;
                }
            }

            LDAPConnection connection = null;
            connection = prepareReferral( u[i], cons );
            if (cons.getRebindProc() == null) {
                connection.authenticate (null, null);
            } else {
                LDAPRebindAuth auth =
                  cons.getRebindProc().getRebindAuthentication(u[i].getHost(),
                  u[i].getPort());
                connection.authenticate(auth.getDN(), auth.getPassword());
            }
            performReferrals(connection, newcons, ops, DN, scope, filter,
              types, attrsOnly, mods, entry, attr, results);
        }
    }

    void performReferrals(LDAPConnection connection, 
      LDAPSearchConstraints cons, int ops, String dn, int scope,
      String filter, String types[], boolean attrsOnly,
      LDAPModification mods[], LDAPEntry entry, LDAPAttribute attr,
      Vector results) throws LDAPException {
 
        LDAPSearchResults res = null;
        try {
            switch (ops) {
                case JDAPProtocolOp.SEARCH_REQUEST:

                    res = connection.search(dn, scope, filter,
                                            types, attrsOnly, cons);
                    if (res != null) {
                        res.closeOnCompletion(connection);
                        results.addElement(res);
                    } else {
                        if ((m_referralConnection == null) ||
                          (!connection.equals(m_referralConnection)))
                            connection.disconnect();
                    }
                    break;
                case JDAPProtocolOp.MODIFY_REQUEST:
                    connection.modify(dn, mods, cons);
                    break;
                case JDAPProtocolOp.ADD_REQUEST:
                    if ((dn != null) && (!dn.equals("")))
                         entry.setDN(dn);
                    connection.add(entry, cons);
                    break;
                case JDAPProtocolOp.DEL_REQUEST:
                    connection.delete(dn, cons);
                    break;
                case JDAPProtocolOp.MODIFY_RDN_REQUEST:
                    connection.rename(dn, filter /* newRDN */, 
                      attrsOnly /* deleteOld */, cons);
                    break;
                case JDAPProtocolOp.COMPARE_REQUEST:
                    boolean bool = connection.compare(dn, attr, cons);
                    results.addElement(new Boolean(bool));
                    break;
                default:
                    /* impossible */
                    break;
            }
        } catch (LDAPException ee) {
            throw ee;
        } finally {
            if ((connection != null) && 
              ((ops != JDAPProtocolOp.SEARCH_REQUEST) || (res == null)) &&
                ((m_referralConnection == null) || 
                 (!connection.equals(m_referralConnection))))
                connection.disconnect();
        }
    }

    /**
     * Performs the referral.
     * @param e referral exception
     * @param cons search constraints
     */
    private LDAPExtendedOperation performExtendedReferrals(
        LDAPReferralException e,
        LDAPSearchConstraints cons, LDAPExtendedOperation op )
        throws LDAPException {

        if (cons.getHopLimit() <= 0)
            throw new LDAPException("exceed hop limit",
              e.getLDAPResultCode(), e.getLDAPErrorMessage());
        if (!cons.getReferrals()) {
            throw e;
        }

        LDAPUrl u[] = e.getURLs();
        // If there are no referrals (because the server isn't set up for
        // them), give up here
        if ( u == null )
            return null;

        for (int i = 0; i < u.length; i++) {
            try {
                LDAPConnection connection = prepareReferral( u[i], cons );
                LDAPExtendedOperation results =
                    connection.extendedOperation( op );
                connection.disconnect();
                return results; /* return right away if operation is successful */
            } catch (LDAPException ee) {
                continue;
            }
        }
        return null;
    }

    /**
     * Creates and returns an new <CODE>LDAPConnection</CODE> object that
     * contains the same information as the current connection, including:
     * <UL>
     * <LI>the default search constraints
     * <LI>host name and port number of the LAAP server
     * <LI>the DN and password used to authenticate to the LDAP server
     * </UL>
     * <P>
     * @return The <CODE>LDAPconnection</CODE> object representing the
     * new object.
     */
    public synchronized Object clone() {
        try {
            LDAPConnection c = (LDAPConnection)super.clone();

            if (this.th == null)
                this.bind(defaultConstraints);

            c.defaultConstraints =
                (LDAPSearchConstraints)defaultConstraints.clone();
            c.responseListeners = null;
            c.searchListeners = null;
            c.bound = this.bound;
            c.host = this.host;
            c.port = this.port;
            c.boundDN = this.boundDN;
            c.boundPasswd = this.boundPasswd;
            c.prevBoundDN = this.prevBoundDN;
            c.prevBoundPasswd = this.prevBoundPasswd;
            c.m_anonymousBound = this.m_anonymousBound;
            c.m_factory = this.m_factory;
            c.th = this.th; /* share current connection thread */

            synchronized (m_threadConnTable) {
                Vector v = (Vector)m_threadConnTable.get(this.th);
                if (v != null) {
                    v.addElement(c);
                }
                else {
                    printDebug("Failed to clone");
                    return null;
                }
            }

            c.th.register(c);
            return c;
        } catch (Exception e) {
        }
        return null;
    }

    /**
     * This is called when a search result has been retrieved from the incoming
     * queue. We use the notification to unblock the listener thread, if it
     * is waiting for the backlog to lighten.
     */
    void resultRetrieved() {
        if ( th != null ) {
            th.resultRetrieved();
        }
    }

    /**
     * Sets up basic connection privileges for Communicator.
     * @return true if in Communicator and connections okay.
     */
    private static boolean checkCommunicator() {
        try {
            java.lang.reflect.Method m = LDAPCheckComm.getMethod(
              "netscape.security.PrivilegeManager", "enablePrivilege");
            if (m == null) {
                printDebug("Method is null");
                return false;
            }

            Object[] args = new Object[1];
            args[0] = new String("UniversalConnect");
            m.invoke( null, args);
            printDebug( "UniversalConnect enabled" );
            return true;
        } catch (LDAPException e) {
            printDebug("Exception: "+e.toString());
        } catch (Exception ie) {
            printDebug("Exception on invoking " + "enablePrivilege: "+ie.toString());
        }
        return false;
    }

    /**
     * Reports if the class is running in a Netscape browser.
     * @return <CODE>true</TRUE> if the class is running in a Netscape browser.
     */
    public static boolean isNetscape() {
        return isCommunicator;
    }

    static void printDebug( String msg ) {
        if ( debug ) {
            System.out.println( msg );
        }
    }

    /**
     * Prints out the LDAP Java SDK version and the highest LDAP
     * protocol version supported by the SDK. To view this
     * information, open a terminal window, and enter:
     * <PRE>java netscape.ldap.LDAPConnection
     * </PRE>
     * @param args Not currently used.
     */
    public static void main(String[] args) {
        System.out.println("LDAP SDK Version is "+SdkVersion);
        System.out.println("LDAP Protocol Version is "+ProtocolVersion);
    }

    /**
     * Option specifying the maximum number of unread entries to be cached in any
     * LDAPSearchResults without suspending reading from the server.
     * @see netscape.ldap.LDAPConnection#getOption
     * @see netscape.ldap.LDAPConnection#setOption
     */
    public static final int MAXBACKLOG   = 30;

    private static boolean isCommunicator = checkCommunicator();
    private static final boolean debug = false;
}

/*
 * This object represents the value of the m_responseControlTable hashtable.
 * It stores the response controls and its corresponding LDAPConnection and
 * the message ID for its corresponding JDAPMessage.
 */
class ResponseControl {
    private LDAPConnection m_connection;
    private int m_messageID;
    private LDAPControl[] m_controls;

    public ResponseControl(LDAPConnection conn, int msgID, 
      LDAPControl[] controls) {

        m_connection = conn;
        m_messageID = msgID;
        m_controls = new LDAPControl[controls.length];
        for (int i=0; i<controls.length; i++) 
            m_controls[i] = controls[i];
    }

    public int getMsgID() {
        return m_messageID;
    }

    public LDAPControl[] getControls() {
        return m_controls;
    }

    public LDAPConnection getConnection() {
        return m_connection;
    }
}

/**
 * Multiple LDAPConnection clones can share a single physical connection,
 * which is maintained by a thread.
 *
 * New Design (09/05/97):
 *
 *   +----------------+
 *   | LDAPConnection | --------+
 *   +----------------+         |
 *                              |
 *   +----------------+         |        +----------------+
 *   | LDAPConnection | --------+------- | LDAPConnThread |
 *   +----------------+         |        +----------------+
 *                              |
 *   +----------------+         |
 *   | LDAPConnection | --------+
 *   +----------------+
 *
 * All LDAPConnections send requests and get responses from
 * LDAPConnThread (a thread).
 */
class LDAPConnThread extends Thread {

    /**
     * Constants
     */
    private final static int MAXMSGID = Integer.MAX_VALUE;

    /**
     * Internal variables
     */
    transient private int m_highMsgId;
    transient private InputStream m_serverInput;
    transient private OutputStream m_serverOutput;
    transient private Hashtable m_requests;
    transient private Hashtable m_messages = null;
    transient private Vector m_registered;
    transient private boolean m_disconnected = false;
    transient private LDAPCache m_cache = null;
    transient private boolean m_failed = false;
    private Object m_securityLayer = null;
    private Socket m_socket = null;
    private int m_maxBacklog = 100;

    /**
     * Constructs a connection thread that maintains connection to the
     * LDAP server.
     * @param host LDAP host name
     * @param port LDAP port number
     * @param factory LDAP socket factory
     */
    public LDAPConnThread(String host, int port, LDAPSocketFactory factory,
      LDAPCache cache) throws LDAPException {
        m_highMsgId = 0;
        m_requests = new Hashtable ();
        m_registered = new Vector ();
        m_cache = cache;
        if (m_cache != null)
            m_messages = new Hashtable ();

        /* Connect to LDAP server */
        try {
            /* If we are to create a socket ourselves, make sure it has
               sufficient privileges to connect to the desired host */
            if (factory == null) {
                m_socket = new Socket (host, port);
            } else {
                m_socket = factory.makeSocket(host, port);
            }
            m_serverInput = new BufferedInputStream (m_socket.getInputStream());
            m_serverOutput = new BufferedOutputStream (m_socket.getOutputStream());
        } catch (IOException e) {

            // a kludge to make the thread go away. Since the thread has already
            // been created, the only way to clean up the thread is to call the
            // start() method. Otherwise, the exit method will be never called
            // because the start() was never called. In the run method, the stop
            // method calls right away if the m_failed is set to true.
            m_failed = true;
            start();
            throw new LDAPException ( "failed to connect to server " + host,
                                      LDAPException.CONNECT_ERROR );
        }
        start(); /* start the thread */
    }

    /**
     * Sends LDAP request via this connection thread.
     * @param request request to send
     * @param toNotify response listener to invoke when the response
     *          is ready
     */
    synchronized void sendRequest (JDAPProtocolOp request,
        LDAPResponseListener toNotify, LDAPSearchConstraints cons)
         throws LDAPException {
        if (m_serverOutput == null)
            throw new LDAPException ( "not connected to a server",
                                  LDAPException.OTHER );

        JDAPMessage msg;
        LDAPControl lc[] = cons.getServerControls();
        if ( (lc != null) && (lc.length > 0) ) {
            JDAPControl[] jc = new JDAPControl[lc.length];
            for( int i = 0; i < lc.length; i++ )
                jc[i] = new JDAPControl( lc[i].getID(),
                                     lc[i].isCritical(),
                                     lc[i].getValue() );
            msg = new JDAPMessage(allocateId(), request, jc);
        }
        else
            msg = new JDAPMessage(allocateId(), request);
        if ( toNotify != null ) {
            if (!(request instanceof JDAPAbandonRequest ||
                  request instanceof JDAPUnbindRequest)) {
                /* Only worry about toNotify if we expect a response... */
                this.m_requests.put (new Integer (msg.getId()), toNotify);
            }
            toNotify.setID( msg.getId() );
        }

        try {
            msg.write (m_serverOutput);
            m_serverOutput.flush ();
        } catch (IOException e) {
            networkError(e);
        }
    }

    /**
     * Register with this connection thread.
     * @param conn LDAP connection
     */
    public synchronized void register(LDAPConnection conn) {
        if (!m_registered.contains(conn))
            m_registered.addElement(conn);
    }

    void setSecurityLayer(Object layer) {
        m_securityLayer = layer;
    }

    synchronized int getClientCount() {
        return m_registered.size();
    }

    /**
     * De-Register with this connection thread. If all the connection
     * is deregistered. Then, this thread should be killed.
     * @param conn LDAP connection
     */
    public synchronized void deregister(LDAPConnection conn) {
        m_registered.removeElement(conn);
        if (m_registered.size() == 0) {
            try {
                LDAPSearchConstraints cons = conn.getSearchConstraints();
                sendRequest (new JDAPUnbindRequest (), null, cons);
                cleanUp();
                this.stop();
                this.sleep(100); /* give enough time for threads to shutdown */
            } catch (Exception e) {
                LDAPConnection.printDebug(e.toString());
            }
        }
    }

    /**
     * Clean ups before shutdown the thread.
     */
    private void cleanUp() {
        if (!m_disconnected) {
            try {
                m_serverOutput.close ();
            } catch (Exception e) {
            } finally {
                m_serverOutput = null;
            }

            try {
                m_serverInput.close ();
            } catch (Exception e) {
            } finally {
                m_serverInput = null;
            }

            try {
                m_socket.close ();
            } catch (Exception e) {
            } finally {
                m_socket = null;
            }
        
            m_disconnected = true;

            /**
             * Notify all the registered about this bad moment.
             * IMPORTANT: This needs to be done at last. Otherwise, the socket
             * input stream and output stream might never get closed and the whole
             * task will get stuck in the stop method when we tried to stop the
             * LDAPConnThread.
             */

            if (m_registered != null) {
                Vector registerCopy = (Vector)m_registered.clone();

                Enumeration cancelled = registerCopy.elements();

                while (cancelled.hasMoreElements ()) {
                    LDAPConnection c = (LDAPConnection)cancelled.nextElement();
                    c.deregisterConnection();
                }
            }
            m_registered = null;
            m_messages = null;
            m_requests.clear();
        }
    }

    /**
     * Set and get the maximum number of unread entries any search listener can
     * have before we stop reading from the server.
     */
    void setMaxBacklog( int backlog ) {
        m_maxBacklog = backlog;
    }

    int getMaxBacklog() {
        return m_maxBacklog;
    }

    /**
     * Sleep if there is a backlog of search results
     */
    private void checkBacklog() {
        boolean doWait;
        do {
            doWait = false;
            Enumeration listeners = m_requests.elements();
            while( listeners.hasMoreElements() ) {
                LDAPResponseListener l =
                    (LDAPResponseListener)listeners.nextElement();
                // If there are any threads waiting for a regular response
                // message, we have to go read the next incoming message
                if ( !(l instanceof LDAPSearchListener) ) {
                    break;
                }
                // If the backlog of any search thread is too great,
                // wait for it to diminish, but if this is a synchronous
                // search, then just keep reading
                LDAPSearchListener sl = (LDAPSearchListener)l;
                if ( (sl.getConstraints().getBatchSize() != 0) &&
                     (sl.getCount() >= m_maxBacklog) ) {
                    doWait = true;
                    break;
                }
            }
            if ( doWait ) {
                synchronized(this) {
                    try {
                        wait();
                    } catch (InterruptedException e ) {
                    }
                }
            }
        } while ( doWait );
    }

    /**
     * This is called when a search result has been retrieved from the incoming
     * queue. We use the notification to unblock the listener thread, if it
     * is waiting for the backlog to lighten.
     */
    synchronized void resultRetrieved() {
        notifyAll();
    }

    /**
     * Reads from the LDAP server input stream for incoming LDAP messages.
     */
    public void run() {
        // if there is a problem of establishing connection to the server,
        // stop the thread right away...
        if (m_failed)
            this.stop();

        JDAPMessage msg = null;
        JDAPBERTagDecoder decoder = new JDAPBERTagDecoder();

        while (true) {
            yield();
            int[] nread = new int[1];
            nread[0] = 0;
            // Avoid too great a backlog of results
            checkBacklog();
            try  {
                BERElement element = BERElement.getElement(decoder, m_serverInput,
                                                           nread);
                msg = new JDAPMessage(element);

                // passed in the ber element size to approximate the size of the cache
                // entry, thereby avoiding serialization of the entry stored in the
                // cache
                processResponse (msg, nread[0]);
            } catch (Exception e)  {
                networkError(e);
            }

            if (m_disconnected)
                break;
        }
    }

    /**
     * Allocates a new LDAP message ID.  These are arbitrary numbers used to
     * correlate client requests with server responses.
     * @return new unique msgId
     */
    private synchronized int allocateId () {
        m_highMsgId = (m_highMsgId + 1) % MAXMSGID;
        return m_highMsgId;
    }

    /**
     * When a response arrives from the LDAP server, it is processed by
     * this routine.  It will pass the message on to the listening object
     * associated with the LDAP msgId.
     * @param incoming New message from LDAP server
     */
    private synchronized void processResponse (JDAPMessage incoming, int size) {
        Integer messageID = new Integer (incoming.getId());
        LDAPResponseListener l = (LDAPResponseListener)m_requests.get (messageID);

        if (l == null) return; /* nobody is waiting for this response (!) */


        /* Were there any controls for this client? */
        LDAPControl[] con = checkControls( incoming );
        ResponseControl responseControl = null;
        if (con != null)
            responseControl = new ResponseControl(l.getConnection(),
              incoming.getId(), con);
        l.getConnection().setResponseControls( this, responseControl );

        JDAPProtocolOp op = incoming.getProtocolOp ();

        Vector v = null;


        if ((op instanceof JDAPSearchResponse) ||
           (op instanceof JDAPSearchResultReference)) {

            ((LDAPSearchListener)l).addSearchResult (incoming);
            Long key = ((LDAPSearchListener)l).getKey();

            if ((m_cache != null) && (key != null)) {
                // get the vector containing the JDAPmessages for the specified messageID
                v = (Vector)m_messages.get(messageID);

                if (v == null)
                {
                    v = new Vector();
                    // keeps track of the total size of all JDAPMessages belonging to the
                    // same messageID, now the size is 0
                    v.addElement(new Long(0));
                }

                // add the size of the current JDAPmessage to the lump sum
                // assume the size of JDAPMessage is more or less the same as the size
                // of LDAPEntry. Eventually LDAPEntry object gets stored in the cache
                // instead of JDAPMessage object.
                long entrySize = ((Long)v.firstElement()).longValue() + size;

                // update the lump sum located in the first element of the vector
                v.setElementAt(new Long(entrySize), 0);

                // convert JDAPMessage object into LDAPEntry which is stored to the
                // end of the Vector
                v.addElement(constructLDAPEntry(incoming));

                // replace the entry
                m_messages.put(messageID, v);
            }
        } else {
            l.setResponse (incoming);
            if (l instanceof LDAPSearchListener) {
                Long key = ((LDAPSearchListener)l).getKey();

                if (key != null) {
                    boolean fail = false;
                    JDAPProtocolOp protocolOp = incoming.getProtocolOp();
                    if (protocolOp instanceof JDAPSearchResult) {
                        JDAPResult res = (JDAPResult)protocolOp;
                        if (res.getResultCode() > 0)
                            fail = true;
                    }

                    if ((!fail) && (m_cache != null))  {

                        // that is. Collects all the JDAPMessages for the specified messageID
                        // no need to keep track of this entry. Remove it.
                        v = (Vector)m_messages.remove(messageID);

                        // If v is null, meaning there are no search results from the
                        // server
                        if (v == null) {
                            v = new Vector();

                            // set the entry size to be 0
                            v.addElement(new Long(0));
                        }

                        try {
                            // add the new entry with key and value (a vector of
                            // LDAPEntry objects)
                            m_cache.addEntry(key, v);
                        } catch (LDAPException e) {
                            System.out.println("Exception: "+e.toString());
                        }
                    }
                }
            }
            m_requests.remove (messageID);
        }
    }

    /**
     * Construct the LDAPEntry from the JDAPMessage object.
     * @param msg The JDAPMessage object
     * @return The LDAPEntry object required for the cache entry.
     */
    private LDAPEntry constructLDAPEntry(JDAPMessage msg) {
        JDAPProtocolOp op = msg.getProtocolOp();
        JDAPSearchResponse sr = (JDAPSearchResponse)op;
        LDAPAttribute[] lattrs = sr.getAttributes();
        LDAPAttributeSet attrs;
        if ( lattrs != null )
            attrs = new LDAPAttributeSet( lattrs );
        else
            attrs = new LDAPAttributeSet();

        String dn = sr.getObjectName();
        return(new LDAPEntry( dn, attrs ));
    }

    /**
     * Extract controls from message and stash them away for retrieval
     * by client.
     * @param response The results of an operation, possibly containing
     * Controls.
     */
    private LDAPControl[] checkControls( JDAPMessage response ) {
        LDAPControl[] con = null;
        if ( response != null ) {
            JDAPControl[] jc = response.getControls();
            if ( (jc != null) && (jc.length > 0) ) {
                con = new LDAPControl[jc.length];
                for( int i = 0; i < jc.length; i++ )
                    con[i] = new LDAPControl( jc[i].getID(),
                                          jc[i].isCritical(),
                                          jc[i].getValue() );
            }
        }
        return con;
    }

    /**
     * Stop dispatching responses for a particular message ID.
     * @param id Message ID for which to discard responses.
     */
    void abandon (int id ) {
        m_requests.remove( new Integer(id) );
    }

    /**
     * Handles network errors.  Basically shuts down the whole connection.
     * @param e The exception which was caught while trying to read from
     * input stream.
     */
    private synchronized void networkError (Exception e) {
        try {

            // notify each listener that the server is down.
            Enumeration requests = m_requests.elements();
            while (requests.hasMoreElements()) {
                LDAPResponseListener listener =
                    (LDAPResponseListener)requests.nextElement();
                listener.setException(new LDAPException("Server down", LDAPException.OTHER));
            }
            cleanUp();

        } catch (NullPointerException ee) {
          System.err.println("Exception: "+ee.toString());
        }

        /**
         * Notify all the registered about this bad moment.
         * IMPORTANT: This needs to be done at last. Otherwise, the socket
         * input stream and output stream might never get closed and the whole
         * task will get stuck in the stop method when we tried to stop the
         * LDAPConnThread.
         */

        if (m_registered != null) {
            Vector registerCopy = (Vector)m_registered.clone();

            Enumeration cancelled = registerCopy.elements();

            while (cancelled.hasMoreElements ()) {
              LDAPConnection c = (LDAPConnection)cancelled.nextElement();
              c.deregisterConnection();
            }
        }
    }
}

