/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.realm;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;

import org.apache.catalina.LifecycleException;
import org.apache.tomcat.util.collections.SynchronizedStack;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSName;

/**
 * <p>Implementation of <strong>Realm</strong> that works with a directory
 * server accessed via the Java Naming and Directory Interface (JNDI) APIs.
 * The following constraints are imposed on the data structure in the
 * underlying directory server:</p>
 * <ul>
 *
 * <li>Each user that can be authenticated is represented by an individual
 *     element in the top level <code>DirContext</code> that is accessed
 *     via the <code>connectionURL</code> property.</li>
 *
 * <li>If a socket connection cannot be made to the <code>connectURL</code>
 *     an attempt will be made to use the <code>alternateURL</code> if it
 *     exists.</li>
 *
 * <li>Each user element has a distinguished name that can be formed by
 *     substituting the presented username into a pattern configured by the
 *     <code>userPattern</code> property.</li>
 *
 * <li>Alternatively, if the <code>userPattern</code> property is not
 *     specified, a unique element can be located by searching the directory
 *     context. In this case:
 *     <ul>
 *     <li>The <code>userSearch</code> pattern specifies the search filter
 *         after substitution of the username.</li>
 *     <li>The <code>userBase</code> property can be set to the element that
 *         is the base of the subtree containing users.  If not specified,
 *         the search base is the top-level context.</li>
 *     <li>The <code>userSubtree</code> property can be set to
 *         <code>true</code> if you wish to search the entire subtree of the
 *         directory context.  The default value of <code>false</code>
 *         requests a search of only the current level.</li>
 *    </ul>
 * </li>
 *
 * <li>The user may be authenticated by binding to the directory with the
 *      username and password presented. This method is used when the
 *      <code>userPassword</code> property is not specified.</li>
 *
 * <li>The user may be authenticated by retrieving the value of an attribute
 *     from the directory and comparing it explicitly with the value presented
 *     by the user. This method is used when the <code>userPassword</code>
 *     property is specified, in which case:
 *     <ul>
 *     <li>The element for this user must contain an attribute named by the
 *         <code>userPassword</code> property.
 *     <li>The value of the user password attribute is either a cleartext
 *         String, or the result of passing a cleartext String through the
 *         <code>RealmBase.digest()</code> method (using the standard digest
 *         support included in <code>RealmBase</code>).
 *     <li>The user is considered to be authenticated if the presented
 *         credentials (after being passed through
 *         <code>RealmBase.digest()</code>) are equal to the retrieved value
 *         for the user password attribute.</li>
 *     </ul></li>
 *
 * <li>Each group of users that has been assigned a particular role may be
 *     represented by an individual element in the top level
 *     <code>DirContext</code> that is accessed via the
 *     <code>connectionURL</code> property.  This element has the following
 *     characteristics:
 *     <ul>
 *     <li>The set of all possible groups of interest can be selected by a
 *         search pattern configured by the <code>roleSearch</code>
 *         property.</li>
 *     <li>The <code>roleSearch</code> pattern optionally includes pattern
 *         replacements "{0}" for the distinguished name, and/or "{1}" for
 *         the username, and/or "{2}" the value of an attribute from the
 *         user's directory entry (the attribute is specified by the
 *         <code>userRoleAttribute</code> property), of the authenticated user
 *         for which roles will be retrieved.</li>
 *     <li>The <code>roleBase</code> property can be set to the element that
 *         is the base of the search for matching roles.  If not specified,
 *         the entire context will be searched.</li>
 *     <li>The <code>roleSubtree</code> property can be set to
 *         <code>true</code> if you wish to search the entire subtree of the
 *         directory context.  The default value of <code>false</code>
 *         requests a search of only the current level.</li>
 *     <li>The element includes an attribute (whose name is configured by
 *         the <code>roleName</code> property) containing the name of the
 *         role represented by this element.</li>
 *     </ul></li>
 *
 * <li>In addition, roles may be represented by the values of an attribute
 * in the user's element whose name is configured by the
 * <code>userRoleName</code> property.</li>
 *
 * <li>A default role can be assigned to each user that was successfully
 * authenticated by setting the <code>commonRole</code> property to the
 * name of this role. The role doesn't have to exist in the directory.</li>
 *
 * <li>If the directory server contains nested roles, you can search for them
 * by setting <code>roleNested</code> to <code>true</code>.
 * The default value is <code>false</code>, so role searches will not find
 * nested roles.</li>
 *
 * <li>Note that the standard <code>&lt;security-role-ref&gt;</code> element in
 *     the web application deployment descriptor allows applications to refer
 *     to roles programmatically by names other than those used in the
 *     directory server itself.</li>
 * </ul>
 *
 * <p><strong>WARNING</strong> - There is a reported bug against the Netscape
 * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to
 * successfully authenticated a non-existing user. The
 * report is here: https://bz.apache.org/bugzilla/show_bug.cgi?id=11210 .
 * With luck, Netscape has updated their provider code and this is not an
 * issue. </p>
 *
 * @author John Holman
 * @author Craig R. McClanahan
 */
public class JNDIRealm extends RealmBase {

    // ----------------------------------------------------- Instance Variables

    /**
     *  The type of authentication to use
     */
    protected String authentication = null;

    /**
     * The connection username for the server we will contact.
     */
    protected String connectionName = null;

    /**
     * The connection password for the server we will contact.
     */
    protected String connectionPassword = null;

    /**
     * The connection URL for the server we will contact.
     */
    protected String connectionURL = null;

    /**
     * The JNDI context factory used to acquire our InitialContext.  By
     * default, assumes use of an LDAP server using the standard JNDI LDAP
     * provider.
     */
    protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";

    /**
     * How aliases should be dereferenced during search operations.
     */
    protected String derefAliases = null;

    /**
     * Constant that holds the name of the environment property for specifying
     * the manner in which aliases should be dereferenced.
     */
    public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases";

    /**
     * The protocol that will be used in the communication with the
     * directory server.
     */
    protected String protocol = null;

    /**
     * Should we ignore PartialResultExceptions when iterating over NamingEnumerations?
     * Microsoft Active Directory often returns referrals, which lead
     * to PartialResultExceptions. Unfortunately there's no stable way to detect,
     * if the Exceptions really come from an AD referral.
     * Set to true to ignore PartialResultExceptions.
     */
    protected boolean adCompat = false;

    /**
     * How should we handle referrals?  Microsoft Active Directory often returns
     * referrals. If you need to follow them set referrals to "follow".
     * Caution: if your DNS is not part of AD, the LDAP client lib might try
     * to resolve your domain name in DNS to find another LDAP server.
     */
    protected String referrals = null;

    /**
     * The base element for user searches.
     */
    protected String userBase = "";

    /**
     * The message format used to search for a user, with "{0}" marking
     * the spot where the username goes.
     */
    protected String userSearch = null;

    /**
     * When searching for users, should the search be performed as the user
     * currently being authenticated? If false, {@link #connectionName} and
     * {@link #connectionPassword} will be used if specified, else an anonymous
     * connection will be used.
     */
    private boolean userSearchAsUser = false;

    /**
     * Should we search the entire subtree for matching users?
     */
    protected boolean userSubtree = false;

    /**
     * The attribute name used to retrieve the user password.
     */
    protected String userPassword = null;

    /**
     * The name of the attribute inside the users
     * directory entry where the value will be
     * taken to search for roles
     * This attribute is not used during a nested search
     */
    protected String userRoleAttribute = null;

    /**
     * A string of LDAP user patterns or paths, ":"-separated
     * These will be used to form the distinguished name of a
     * user, with "{0}" marking the spot where the specified username
     * goes.
     * This is similar to userPattern, but allows for multiple searches
     * for a user.
     */
    protected String[] userPatternArray = null;

    /**
     * The message format used to form the distinguished name of a
     * user, with "{0}" marking the spot where the specified username
     * goes.
     */
    protected String userPattern = null;

    /**
     * The base element for role searches.
     */
    protected String roleBase = "";

    /**
     * The name of an attribute in the user's entry containing
     * roles for that user
     */
    protected String userRoleName = null;

    /**
     * The name of the attribute containing roles held elsewhere
     */
    protected String roleName = null;

    /**
     * The message format used to select roles for a user, with "{0}" marking
     * the spot where the distinguished name of the user goes. The "{1}"
     * and "{2}" are described in the Configuration Reference.
     */
    protected String roleSearch = null;

    /**
     * Should we search the entire subtree for matching memberships?
     */
    protected boolean roleSubtree = false;

    /**
     * Should we look for nested group in order to determine roles?
     */
    protected boolean roleNested = false;

    /**
     * When searching for user roles, should the search be performed as the user
     * currently being authenticated? If false, {@link #connectionName} and
     * {@link #connectionPassword} will be used if specified, else an anonymous
     * connection will be used.
     */
    protected boolean roleSearchAsUser = false;

    /**
     * An alternate URL, to which, we should connect if connectionURL fails.
     */
    protected String alternateURL;

    /**
     * The number of connection attempts.  If greater than zero we use the
     * alternate url.
     */
    protected int connectionAttempt = 0;

    /**
     *  Add this role to every authenticated user
     */
    protected String commonRole = null;

    /**
     * The timeout, in milliseconds, to use when trying to create a connection
     * to the directory. The default is 5000 (5 seconds).
     */
    protected String connectionTimeout = "5000";

    /**
     * The timeout, in milliseconds, to use when trying to read from a connection
     * to the directory. The default is 5000 (5 seconds).
     */
    protected String readTimeout = "5000";

    /**
     * The sizeLimit (also known as the countLimit) to use when the realm is
     * configured with {@link #userSearch}. Zero for no limit.
     */
    protected long sizeLimit = 0;

    /**
     * The timeLimit (in milliseconds) to use when the realm is configured with
     * {@link #userSearch}. Zero for no limit.
     */
    protected int timeLimit = 0;

    /**
     * Should delegated credentials from the SPNEGO authenticator be used if
     * available
     */
    protected boolean useDelegatedCredential = true;

    /**
     * The QOP that should be used for the connection to the LDAP server after
     * authentication. This value is used to set the
     * <code>javax.security.sasl.qop</code> environment property for the LDAP
     * connection.
     */
    protected String spnegoDelegationQop = "auth-conf";

    /**
     * Whether to use TLS for connections
     */
    private boolean useStartTls = false;

    private StartTlsResponse tls = null;

    /**
     * The list of enabled cipher suites used for establishing tls connections.
     * <code>null</code> means to use the default cipher suites.
     */
    private String[] cipherSuitesArray = null;

    /**
     * Verifier for hostnames in a StartTLS secured connection. <code>null</code>
     * means to use the default verifier.
     */
    private HostnameVerifier hostnameVerifier = null;

    /**
     * {@link SSLSocketFactory} to use when connection with StartTLS enabled.
     */
    private SSLSocketFactory sslSocketFactory = null;

    /**
     * Name of the class of the {@link SSLSocketFactory}. <code>null</code>
     * means to use the default factory.
     */
    private String sslSocketFactoryClassName;

    /**
     * Comma separated list of cipher suites to use for StartTLS. If empty, the
     * default suites are used.
     */
    private String cipherSuites;

    /**
     * Name of the class of the {@link HostnameVerifier}. <code>null</code>
     * means to use the default verifier.
     */
    private String hostNameVerifierClassName;

    /**
     * The ssl Protocol which will be used by StartTLS.
     */
    private String sslProtocol;

    private boolean forceDnHexEscape = false;

    /**
     * Non pooled connection to our directory server.
     */
    protected JNDIConnection singleConnection;

    /**
     * The lock to ensure single connection thread safety.
     */
    protected final Lock singleConnectionLock = new ReentrantLock();

    /**
     * Connection pool.
     */
    protected SynchronizedStack<JNDIConnection> connectionPool = null;

    /**
     * The pool size limit. If 1, pooling is not used.
     */
    protected int connectionPoolSize = 1;

    /**
     * Whether to use context ClassLoader or default ClassLoader.
     * True means use context ClassLoader, and True is the default
     * value.
     */
    protected boolean useContextClassLoader = true;


    // ------------------------------------------------------------- Properties

    public boolean getForceDnHexEscape() {
        return forceDnHexEscape;
    }


    public void setForceDnHexEscape(boolean forceDnHexEscape) {
        this.forceDnHexEscape = forceDnHexEscape;
    }


    /**
     * @return the type of authentication to use.
     */
    public String getAuthentication() {
        return authentication;
    }


    /**
     * Set the type of authentication to use.
     *
     * @param authentication The authentication
     */
    public void setAuthentication(String authentication) {
        this.authentication = authentication;
    }


    /**
     * @return the connection username for this Realm.
     */
    public String getConnectionName() {
        return this.connectionName;
    }


    /**
     * Set the connection username for this Realm.
     *
     * @param connectionName The new connection username
     */
    public void setConnectionName(String connectionName) {
        this.connectionName = connectionName;
    }


    /**
     * @return the connection password for this Realm.
     */
    public String getConnectionPassword() {
        return this.connectionPassword;
    }


    /**
     * Set the connection password for this Realm.
     *
     * @param connectionPassword The new connection password
     */
    public void setConnectionPassword(String connectionPassword) {
        this.connectionPassword = connectionPassword;
    }


    /**
     * @return the connection URL for this Realm.
     */
    public String getConnectionURL() {
        return this.connectionURL;
    }


    /**
     * Set the connection URL for this Realm.
     *
     * @param connectionURL The new connection URL
     */
    public void setConnectionURL(String connectionURL) {
        this.connectionURL = connectionURL;
    }


    /**
     * @return the JNDI context factory for this Realm.
     */
    public String getContextFactory() {
        return this.contextFactory;
    }


    /**
     * Set the JNDI context factory for this Realm.
     *
     * @param contextFactory The new context factory
     */
    public void setContextFactory(String contextFactory) {
        this.contextFactory = contextFactory;
    }


    /**
     * @return the derefAliases setting to be used.
     */
    public java.lang.String getDerefAliases() {
        return derefAliases;
    }


    /**
     * Set the value for derefAliases to be used when searching the directory.
     *
     * @param derefAliases New value of property derefAliases.
     */
    public void setDerefAliases(java.lang.String derefAliases) {
        this.derefAliases = derefAliases;
    }


    /**
     * @return the protocol to be used.
     */
    public String getProtocol() {
        return protocol;
    }


    /**
     * Set the protocol for this Realm.
     *
     * @param protocol The new protocol.
     */
    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }


    /**
     * @return the current settings for handling PartialResultExceptions
     */
    public boolean getAdCompat () {
        return adCompat;
    }


    /**
     * How do we handle PartialResultExceptions?
     * True: ignore all PartialResultExceptions.
     * @param adCompat <code>true</code> to ignore partial results
     */
    public void setAdCompat (boolean adCompat) {
        this.adCompat = adCompat;
    }


    /**
     * @return the current settings for handling JNDI referrals.
     */
    public String getReferrals () {
        return referrals;
    }


    /**
     * How do we handle JNDI referrals? ignore, follow, or throw
     * (see javax.naming.Context.REFERRAL for more information).
     * @param referrals The referral handling
     */
    public void setReferrals (String referrals) {
        this.referrals = referrals;
    }


    /**
     * @return the base element for user searches.
     */
    public String getUserBase() {
        return this.userBase;
    }


    /**
     * Set the base element for user searches.
     *
     * @param userBase The new base element
     */
    public void setUserBase(String userBase) {
        this.userBase = userBase;
    }


    /**
     * @return the message format pattern for selecting users in this Realm.
     */
    public String getUserSearch() {
        return this.userSearch;
    }


    /**
     * Set the message format pattern for selecting users in this Realm.
     *
     * @param userSearch The new user search pattern
     */
    public void setUserSearch(String userSearch) {
        this.userSearch = userSearch;
        singleConnection = create();
    }


    public boolean isUserSearchAsUser() {
        return userSearchAsUser;
    }


    public void setUserSearchAsUser(boolean userSearchAsUser) {
        this.userSearchAsUser = userSearchAsUser;
    }


    /**
     * @return the "search subtree for users" flag.
     */
    public boolean getUserSubtree() {
        return this.userSubtree;
    }


    /**
     * Set the "search subtree for users" flag.
     *
     * @param userSubtree The new search flag
     */
    public void setUserSubtree(boolean userSubtree) {
        this.userSubtree = userSubtree;
    }


    /**
     * @return the user role name attribute name for this Realm.
     */
    public String getUserRoleName() {
        return userRoleName;
    }


    /**
     * Set the user role name attribute name for this Realm.
     *
     * @param userRoleName The new userRole name attribute name
     */
    public void setUserRoleName(String userRoleName) {
        this.userRoleName = userRoleName;
    }


    /**
     * @return the base element for role searches.
     */
    public String getRoleBase() {
        return this.roleBase;
    }


    /**
     * Set the base element for role searches.
     *
     * @param roleBase The new base element
     */
    public void setRoleBase(String roleBase) {
        this.roleBase = roleBase;
        singleConnection = create();
    }


    /**
     * @return the role name attribute name for this Realm.
     */
    public String getRoleName() {
        return this.roleName;
    }


    /**
     * Set the role name attribute name for this Realm.
     *
     * @param roleName The new role name attribute name
     */
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }


    /**
     * @return the message format pattern for selecting roles in this Realm.
     */
    public String getRoleSearch() {
        return this.roleSearch;
    }


    /**
     * Set the message format pattern for selecting roles in this Realm.
     *
     * @param roleSearch The new role search pattern
     */
    public void setRoleSearch(String roleSearch) {
        this.roleSearch = roleSearch;
        singleConnection = create();
    }


    public boolean isRoleSearchAsUser() {
        return roleSearchAsUser;
    }


    public void setRoleSearchAsUser(boolean roleSearchAsUser) {
        this.roleSearchAsUser = roleSearchAsUser;
    }


    /**
     * @return the "search subtree for roles" flag.
     */
    public boolean getRoleSubtree() {
        return this.roleSubtree;
    }


    /**
     * Set the "search subtree for roles" flag.
     *
     * @param roleSubtree The new search flag
     */
    public void setRoleSubtree(boolean roleSubtree) {
        this.roleSubtree = roleSubtree;
    }


    /**
     * @return the "The nested group search flag" flag.
     */
    public boolean getRoleNested() {
        return this.roleNested;
    }


    /**
     * Set the "search subtree for roles" flag.
     *
     * @param roleNested The nested group search flag
     */
    public void setRoleNested(boolean roleNested) {
        this.roleNested = roleNested;
    }


    /**
     * @return the password attribute used to retrieve the user password.
     */
    public String getUserPassword() {
        return this.userPassword;
    }


    /**
     * Set the password attribute used to retrieve the user password.
     *
     * @param userPassword The new password attribute
     */
    public void setUserPassword(String userPassword) {
        this.userPassword = userPassword;
    }


    public String getUserRoleAttribute() {
        return userRoleAttribute;
    }


    public void setUserRoleAttribute(String userRoleAttribute) {
        this.userRoleAttribute = userRoleAttribute;
    }

    /**
     * @return the message format pattern for selecting users in this Realm.
     */
    public String getUserPattern() {
        return this.userPattern;
    }


    /**
     * Set the message format pattern for selecting users in this Realm.
     * This may be one simple pattern, or multiple patterns to be tried,
     * separated by parentheses. (for example, either "cn={0}", or
     * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
     * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
     * also valid. Complex search strings with &amp;, etc are NOT supported.
     *
     * @param userPattern The new user pattern
     */
    public void setUserPattern(String userPattern) {
        this.userPattern = userPattern;
        if (userPattern == null) {
            userPatternArray = null;
        } else {
            userPatternArray = parseUserPatternString(userPattern);
            singleConnection = create();
        }
    }


    /**
     * Getter for property alternateURL.
     *
     * @return Value of property alternateURL.
     */
    public String getAlternateURL() {
        return this.alternateURL;
    }


    /**
     * Setter for property alternateURL.
     *
     * @param alternateURL New value of property alternateURL.
     */
    public void setAlternateURL(String alternateURL) {
        this.alternateURL = alternateURL;
    }


    /**
     * @return the common role
     */
    public String getCommonRole() {
        return commonRole;
    }


    /**
     * Set the common role
     *
     * @param commonRole The common role
     */
    public void setCommonRole(String commonRole) {
        this.commonRole = commonRole;
    }


    /**
     * @return the connection timeout.
     */
    public String getConnectionTimeout() {
        return connectionTimeout;
    }


    /**
     * Set the connection timeout.
     *
     * @param timeout The new connection timeout
     */
    public void setConnectionTimeout(String timeout) {
        this.connectionTimeout = timeout;
    }


    /**
     * @return the read timeout.
     */
    public String getReadTimeout() {
        return readTimeout;
    }


    /**
     * Set the read timeout.
     *
     * @param timeout The new read timeout
     */
    public void setReadTimeout(String timeout) {
        this.readTimeout = timeout;
    }


    public long getSizeLimit() {
        return sizeLimit;
    }


    public void setSizeLimit(long sizeLimit) {
        this.sizeLimit = sizeLimit;
    }


    public int getTimeLimit() {
        return timeLimit;
    }


    public void setTimeLimit(int timeLimit) {
        this.timeLimit = timeLimit;
    }


    public boolean isUseDelegatedCredential() {
        return useDelegatedCredential;
    }


    public void setUseDelegatedCredential(boolean useDelegatedCredential) {
        this.useDelegatedCredential = useDelegatedCredential;
    }


    public String getSpnegoDelegationQop() {
        return spnegoDelegationQop;
    }


    public void setSpnegoDelegationQop(String spnegoDelegationQop) {
        this.spnegoDelegationQop = spnegoDelegationQop;
    }


    /**
     * @return flag whether to use StartTLS for connections to the ldap server
     */
    public boolean getUseStartTls() {
        return useStartTls;
    }


    /**
     * Flag whether StartTLS should be used when connecting to the ldap server
     *
     * @param useStartTls
     *            {@code true} when StartTLS should be used. Default is
     *            {@code false}.
     */
    public void setUseStartTls(boolean useStartTls) {
        this.useStartTls = useStartTls;
    }


    /**
     * @return list of the allowed cipher suites when connections are made using
     *         StartTLS
     */
    private String[] getCipherSuitesArray() {
        if (cipherSuites == null || cipherSuitesArray != null) {
            return cipherSuitesArray;
        }
        if (this.cipherSuites.trim().isEmpty()) {
            containerLog.warn(sm.getString("jndiRealm.emptyCipherSuites"));
            this.cipherSuitesArray = null;
        } else {
            this.cipherSuitesArray = cipherSuites.trim().split("\\s*,\\s*");
            containerLog.debug(sm.getString("jndiRealm.cipherSuites",
                    Arrays.toString(this.cipherSuitesArray)));
        }
        return this.cipherSuitesArray;
    }


    /**
     * Set the allowed cipher suites when opening a connection using StartTLS.
     * The cipher suites are expected as a comma separated list.
     *
     * @param suites
     *            comma separated list of allowed cipher suites
     */
    public void setCipherSuites(String suites) {
        this.cipherSuites = suites;
    }


    /**
     * @return the connection pool size, or the default value 1 if pooling
     *   is disabled
     */
    public int getConnectionPoolSize() {
        return connectionPoolSize;
    }


    /**
     * Set the connection pool size
     * @param connectionPoolSize the new pool size
     */
    public void setConnectionPoolSize(int connectionPoolSize) {
        this.connectionPoolSize = connectionPoolSize;
    }


    /**
     * @return name of the {@link HostnameVerifier} class used for connections
     *         using StartTLS, or the empty string, if the default verifier
     *         should be used.
     */
    public String getHostnameVerifierClassName() {
        if (this.hostnameVerifier == null) {
            return "";
        }
        return this.hostnameVerifier.getClass().getCanonicalName();
    }


    /**
     * Set the {@link HostnameVerifier} to be used when opening connections
     * using StartTLS. An instance of the given class name will be constructed
     * using the default constructor.
     *
     * @param verifierClassName
     *            class name of the {@link HostnameVerifier} to be constructed
     */
    public void setHostnameVerifierClassName(String verifierClassName) {
        if (verifierClassName != null) {
            this.hostNameVerifierClassName = verifierClassName.trim();
        } else {
            this.hostNameVerifierClassName = null;
        }
    }


    /**
     * @return the {@link HostnameVerifier} to use for peer certificate
     *         verification when opening connections using StartTLS.
     */
    public HostnameVerifier getHostnameVerifier() {
        if (this.hostnameVerifier != null) {
            return this.hostnameVerifier;
        }
        if (this.hostNameVerifierClassName == null || hostNameVerifierClassName.equals("")) {
            return null;
        }
        try {
            Object o = constructInstance(hostNameVerifierClassName);
            if (o instanceof HostnameVerifier) {
                this.hostnameVerifier = (HostnameVerifier) o;
                return this.hostnameVerifier;
            } else {
                throw new IllegalArgumentException(sm.getString(
                        "jndiRealm.invalidHostnameVerifier",
                        hostNameVerifierClassName));
            }
        } catch (ReflectiveOperationException | SecurityException e) {
            throw new IllegalArgumentException(sm.getString(
                    "jndiRealm.invalidHostnameVerifier",
                    hostNameVerifierClassName), e);
        }
    }


    /**
     * Set the {@link SSLSocketFactory} to be used when opening connections
     * using StartTLS. An instance of the factory with the given name will be
     * created using the default constructor. The SSLSocketFactory can also be
     * set using {@link JNDIRealm#setSslProtocol(String) setSslProtocol(String)}.
     *
     * @param factoryClassName
     *            class name of the factory to be constructed
     */
    public void setSslSocketFactoryClassName(String factoryClassName) {
        this.sslSocketFactoryClassName = factoryClassName;
    }


    /**
     * Set the ssl protocol to be used for connections using StartTLS.
     *
     * @param protocol
     *            one of the allowed ssl protocol names
     */
    public void setSslProtocol(String protocol) {
        this.sslProtocol = protocol;
    }


    /**
     * @return the list of supported ssl protocols by the default
     *         {@link SSLContext}
     */
    private String[] getSupportedSslProtocols() {
        try {
            SSLContext sslContext = SSLContext.getDefault();
            return sslContext.getSupportedSSLParameters().getProtocols();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(sm.getString("jndiRealm.exception"), e);
        }
    }


    private Object constructInstance(String className)
            throws ReflectiveOperationException {
        Class<?> clazz = Class.forName(className);
        return clazz.getConstructor().newInstance();
    }


    /**
     * Sets whether to use the context or default ClassLoader.
     * True means use context ClassLoader.
     *
     * @param useContext True means use context ClassLoader
     */
    public void setUseContextClassLoader(boolean useContext) {
        useContextClassLoader = useContext;
    }


    /**
     * Returns whether to use the context or default ClassLoader.
     * True means to use the context ClassLoader.
     *
     * @return The value of useContextClassLoader
     */
    public boolean isUseContextClassLoader() {
        return useContextClassLoader;
    }


    // ---------------------------------------------------------- Realm Methods

    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * If there are any errors with the JDBC connection, executing
     * the query or anything we return null (don't authenticate). This
     * event is also logged, and the connection will be closed so that
     * a subsequent request will automatically re-open it.
     *
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *  authenticating this username
     * @return the associated principal, or <code>null</code> if there is none.
     */
    @Override
    public Principal authenticate(String username, String credentials) {

        ClassLoader ocl = null;
        JNDIConnection connection = null;
        Principal principal = null;

        try {
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
            // This can move back to open() once it is known that Tomcat must be
            // running on a JVM that includes a fix for
            // https://bugs.openjdk.java.net/browse/JDK-8273874
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }

            // Ensure that we have a directory context available
            connection = get();

            try {

                // Occasionally the directory context will timeout.  Try one more
                // time before giving up.

                // Authenticate the specified username if possible
                principal = authenticate(connection, username, credentials);

            } catch (NullPointerException | NamingException e) {
                /*
                 * BZ 61313
                 * NamingException may or may not indicate an error that is
                 * recoverable via fail over. Therefore a decision needs to be
                 * made whether to fail over or not. Generally, attempting to
                 * fail over when it is not appropriate is better than not
                 * failing over when it is appropriate so the code always
                 * attempts to fail over for NamingExceptions.
                 */

                /*
                 * BZ 42449
                 * Catch NPE - Kludge Sun's LDAP provider with broken SSL.
                 */

                // log the exception so we know it's there.
                containerLog.info(sm.getString("jndiRealm.exception.retry"), e);

                // close the connection so we know it will be reopened.
                close(connection);
                closePooledConnections();

                // open a new directory context.
                connection = get();

                // Try the authentication again.
                principal = authenticate(connection, username, credentials);
            }


            // Release this context
            release(connection);

            // Return the authenticated Principal (if any)
            return principal;

        } catch (Exception e) {

            // Log the problem for posterity
            containerLog.error(sm.getString("jndiRealm.exception"), e);

            // close the connection so we know it will be reopened.
            close(connection);
            closePooledConnections();

            // Return "not authenticated" for this request
            if (containerLog.isDebugEnabled()) {
                containerLog.debug("Returning null principal.");
            }
            return null;
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     *
     * @param connection The directory context
     * @param username Username of the Principal to look up
     * @param credentials Password or other credentials to use in
     *  authenticating this username
     * @return the associated principal, or <code>null</code> if there is none.
     *
     * @exception NamingException if a directory server error occurs
     */
    public Principal authenticate(JNDIConnection connection, String username, String credentials)
            throws NamingException {

        if (username == null || username.equals("") || credentials == null || credentials.equals("")) {
            if (containerLog.isDebugEnabled()) {
                containerLog.debug("username null or empty: returning null principal.");
            }
            return null;
        }

        ClassLoader ocl = null;
        try {
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
            // This can move back to open() once it is known that Tomcat must be
            // running on a JVM that includes a fix for
            // https://bugs.openjdk.java.net/browse/JDK-8273874
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }

            if (userPatternArray != null) {
                for (int curUserPattern = 0; curUserPattern < userPatternArray.length; curUserPattern++) {
                    // Retrieve user information
                    User user = getUser(connection, username, credentials, curUserPattern);
                    if (user != null) {
                        try {
                            // Check the user's credentials
                            if (checkCredentials(connection.context, user, credentials)) {
                                // Search for additional roles
                                List<String> roles = getRoles(connection, user);
                                if (containerLog.isDebugEnabled()) {
                                    containerLog.debug("Found roles: " + roles.toString());
                                }
                                return new GenericPrincipal(username, roles);
                            }
                        } catch (InvalidNameException ine) {
                            // Log the problem for posterity
                            containerLog.warn(sm.getString("jndiRealm.exception"), ine);
                            // ignore; this is probably due to a name not fitting
                            // the search path format exactly, as in a fully-
                            // qualified name being munged into a search path
                            // that already contains cn= or vice-versa
                        }
                    }
                }
                return null;
            } else {
                // Retrieve user information
                User user = getUser(connection, username, credentials);
                if (user == null) {
                    return null;
                }

                // Check the user's credentials
                if (!checkCredentials(connection.context, user, credentials)) {
                    return null;
                }

                // Search for additional roles
                List<String> roles = getRoles(connection, user);
                if (containerLog.isDebugEnabled()) {
                    containerLog.debug("Found roles: " + roles.toString());
                }

                // Create and return a suitable Principal for this user
                return new GenericPrincipal(username, roles);
            }
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
     * This method can be removed and the class loader switch moved back to
     * open() once it is known that Tomcat must be running on a JVM that
     * includes a fix for
     * https://bugs.openjdk.java.net/browse/JDK-8273874
     */
    @Override
    public Principal authenticate(String username) {
        ClassLoader ocl = null;
        try {
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            return super.authenticate(username);
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
     * This method can be removed and the class loader switch moved back to
     * open() once it is known that Tomcat must be running on a JVM that
     * includes a fix for
     * https://bugs.openjdk.java.net/browse/JDK-8273874
     */
    @Override
    public Principal authenticate(String username, String clientDigest, String nonce, String nc,
            String cnonce, String qop, String realm, String md5a2) {
        ClassLoader ocl = null;
        try {
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            return super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, md5a2);
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
     * This method can be removed and the class loader switch moved back to
     * open() once it is known that Tomcat must be running on a JVM that
     * includes a fix for
     * https://bugs.openjdk.java.net/browse/JDK-8273874
     */
    @Override
    public Principal authenticate(X509Certificate[] certs) {
        ClassLoader ocl = null;
        try {
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            return super.authenticate(certs);
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
     * This method can be removed and the class loader switch moved back to
     * open() once it is known that Tomcat must be running on a JVM that
     * includes a fix for
     * https://bugs.openjdk.java.net/browse/JDK-8273874
     */
    @Override
    public Principal authenticate(GSSContext gssContext, boolean storeCred) {
        ClassLoader ocl = null;
        try {
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            return super.authenticate(gssContext, storeCred);
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
     * This method can be removed and the class loader switch moved back to
     * open() once it is known that Tomcat must be running on a JVM that
     * includes a fix for
     * https://bugs.openjdk.java.net/browse/JDK-8273874
     */
    @Override
    public Principal authenticate(GSSName gssName, GSSCredential gssCredential) {
        ClassLoader ocl = null;
        try {
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            return super.authenticate(gssName, gssCredential);
        } finally {
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }
    }


    // ------------------------------------------------------ Protected Methods

    /**
     * Return a User object containing information about the user
     * with the specified username, if found in the directory;
     * otherwise return <code>null</code>.
     *
     * @param connection The directory context
     * @param username Username to be looked up
     * @return the User object
     * @exception NamingException if a directory server error occurs
     *
     * @see #getUser(JNDIConnection, String, String, int)
     */
    protected User getUser(JNDIConnection connection, String username) throws NamingException {
        return getUser(connection, username, null, -1);
    }


    /**
     * Return a User object containing information about the user
     * with the specified username, if found in the directory;
     * otherwise return <code>null</code>.
     *
     * @param connection The directory context
     * @param username Username to be looked up
     * @param credentials User credentials (optional)
     * @return the User object
     * @exception NamingException if a directory server error occurs
     *
     * @see #getUser(JNDIConnection, String, String, int)
     */
    protected User getUser(JNDIConnection connection, String username, String credentials) throws NamingException {
        return getUser(connection, username, credentials, -1);
    }


    /**
     * Return a User object containing information about the user
     * with the specified username, if found in the directory;
     * otherwise return <code>null</code>.
     *
     * If the <code>userPassword</code> configuration attribute is
     * specified, the value of that attribute is retrieved from the
     * user's directory entry. If the <code>userRoleName</code>
     * configuration attribute is specified, all values of that
     * attribute are retrieved from the directory entry.
     *
     * @param connection The directory context
     * @param username Username to be looked up
     * @param credentials User credentials (optional)
     * @param curUserPattern Index into userPatternFormatArray
     * @return the User object
     * @exception NamingException if a directory server error occurs
     */
    protected User getUser(JNDIConnection connection, String username, String credentials, int curUserPattern)
            throws NamingException {

        User user = null;

        // Get attributes to retrieve from user entry
        List<String> list = new ArrayList<>();
        if (userPassword != null) {
            list.add(userPassword);
        }
        if (userRoleName != null) {
            list.add(userRoleName);
        }
        if (userRoleAttribute != null) {
            list.add(userRoleAttribute);
        }
        String[] attrIds = new String[list.size()];
        list.toArray(attrIds);

        // Use pattern or search for user entry
        if (userPatternArray != null && curUserPattern >= 0) {
            user = getUserByPattern(connection, username, credentials, attrIds, curUserPattern);
            if (containerLog.isDebugEnabled()) {
                containerLog.debug("Found user by pattern [" + user + "]");
            }
        } else {
            boolean thisUserSearchAsUser = isUserSearchAsUser();
            try {
                if (thisUserSearchAsUser) {
                    userCredentialsAdd(connection.context, username, credentials);
                }
                user = getUserBySearch(connection, username, attrIds);
            } finally {
                if (thisUserSearchAsUser) {
                    userCredentialsRemove(connection.context);
                }
            }
            if (containerLog.isDebugEnabled()) {
                containerLog.debug("Found user by search [" + user + "]");
            }
        }
        if (userPassword == null && credentials != null && user != null) {
            // The password is available. Insert it since it may be required for
            // role searches.
            return new User(user.getUserName(), user.getDN(), credentials, user.getRoles(), user.getUserRoleId());
        }

        return user;
    }


    /**
     * Use the distinguished name to locate the directory
     * entry for the user with the specified username and
     * return a User object; otherwise return <code>null</code>.
     *
     * @param context The directory context
     * @param username The username
     * @param attrIds String[]containing names of attributes to
     * @param dn Distinguished name of the user
     * retrieve.
     * @return the User object
     * @exception NamingException if a directory server error occurs
     */
    protected User getUserByPattern(DirContext context, String username, String[] attrIds, String dn)
            throws NamingException {

        // If no attributes are requested, no need to look for them
        if (attrIds == null || attrIds.length == 0) {
            return new User(username, dn, null, null,null);
        }

        // Get required attributes from user entry
        Attributes attrs = null;
        try {
            attrs = context.getAttributes(dn, attrIds);
        } catch (NameNotFoundException e) {
            return null;
        }
        if (attrs == null) {
            return null;
        }

        // Retrieve value of userPassword
        String password = null;
        if (userPassword != null) {
            password = getAttributeValue(userPassword, attrs);
        }

        String userRoleAttrValue = null;
        if (userRoleAttribute != null) {
            userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs);
        }

        // Retrieve values of userRoleName attribute
        ArrayList<String> roles = null;
        if (userRoleName != null) {
            roles = addAttributeValues(userRoleName, attrs, roles);
        }

        return new User(username, dn, password, roles, userRoleAttrValue);
    }


    /**
     * Use the <code>UserPattern</code> configuration attribute to
     * locate the directory entry for the user with the specified
     * username and return a User object; otherwise return
     * <code>null</code>.
     *
     * @param connection The directory context
     * @param username The username
     * @param credentials User credentials (optional)
     * @param attrIds String[]containing names of attributes to
     * @param curUserPattern Index into userPatternFormatArray
     * @return the User object
     * @exception NamingException if a directory server error occurs
     * @see #getUserByPattern(DirContext, String, String[], String)
     */
    protected User getUserByPattern(JNDIConnection connection, String username, String credentials, String[] attrIds,
            int curUserPattern) throws NamingException {

        User user = null;

        if (username == null || userPatternArray[curUserPattern] == null) {
            return null;
        }

        // Form the DistinguishedName from the user pattern.
        // Escape in case username contains a character with special meaning in
        // an attribute value.
        String dn = connection.userPatternFormatArray[curUserPattern].format(
                new String[] { doAttributeValueEscaping(username) });

        try {
            user = getUserByPattern(connection.context, username, attrIds, dn);
        } catch (NameNotFoundException e) {
            return null;
        } catch (NamingException e) {
            // If the getUserByPattern() call fails, try it again with the
            // credentials of the user that we're searching for
            try {
                userCredentialsAdd(connection.context, dn, credentials);

                user = getUserByPattern(connection.context, username, attrIds, dn);
            } finally {
                userCredentialsRemove(connection.context);
            }
        }
        return user;
    }


    /**
     * Search the directory to return a User object containing
     * information about the user with the specified username, if
     * found in the directory; otherwise return <code>null</code>.
     *
     * @param connection The directory context
     * @param username The username
     * @param attrIds String[]containing names of attributes to retrieve.
     * @return the User object
     * @exception NamingException if a directory server error occurs
     */
    protected User getUserBySearch(JNDIConnection connection, String username, String[] attrIds)
            throws NamingException {

        if (username == null || connection.userSearchFormat == null) {
            return null;
        }

        // Form the search filter
        // Escape in case username contains a character with special meaning in
        // a search filter.
        String filter = connection.userSearchFormat.format(new String[] { doFilterEscaping(username) });

        // Set up the search controls
        SearchControls constraints = new SearchControls();

        if (userSubtree) {
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
        } else {
            constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
        }

        constraints.setCountLimit(sizeLimit);
        constraints.setTimeLimit(timeLimit);

        // Specify the attributes to be retrieved
        if (attrIds == null) {
            attrIds = new String[0];
        }
        constraints.setReturningAttributes(attrIds);

        NamingEnumeration<SearchResult> results = connection.context.search(userBase, filter, constraints);

        try {
            // Fail if no entries found
            try {
                if (results == null || !results.hasMore()) {
                    return null;
                }
            } catch (PartialResultException ex) {
                if (!adCompat) {
                    throw ex;
                } else {
                    return null;
                }
            }

            // Get result for the first entry found
            SearchResult result = results.next();

            // Check no further entries were found
            try {
                if (results.hasMore()) {
                    if (containerLog.isInfoEnabled()) {
                        containerLog.info(sm.getString("jndiRealm.multipleEntries", username));
                    }
                    return null;
                }
            } catch (PartialResultException ex) {
                if (!adCompat) {
                    throw ex;
                }
            }

            String dn = getDistinguishedName(connection.context, userBase, result);

            if (containerLog.isTraceEnabled()) {
                containerLog.trace("  entry found for " + username + " with dn " + dn);
            }

            // Get the entry's attributes
            Attributes attrs = result.getAttributes();
            if (attrs == null) {
                return null;
            }

            // Retrieve value of userPassword
            String password = null;
            if (userPassword != null) {
                password = getAttributeValue(userPassword, attrs);
            }

            String userRoleAttrValue = null;
            if (userRoleAttribute != null) {
                userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs);
            }

            // Retrieve values of userRoleName attribute
            ArrayList<String> roles = null;
            if (userRoleName != null) {
                roles = addAttributeValues(userRoleName, attrs, roles);
            }

            return new User(username, dn, password, roles, userRoleAttrValue);
        } finally {
            if (results != null) {
                results.close();
            }
        }
    }


    /**
     * Check whether the given User can be authenticated with the
     * given credentials. If the <code>userPassword</code>
     * configuration attribute is specified, the credentials
     * previously retrieved from the directory are compared explicitly
     * with those presented by the user. Otherwise the presented
     * credentials are checked by binding to the directory as the
     * user.
     *
     * @param context The directory context
     * @param user The User to be authenticated
     * @param credentials The credentials presented by the user
     * @return <code>true</code> if the credentials are validated
     * @exception NamingException if a directory server error occurs
     */
    protected boolean checkCredentials(DirContext context, User user, String credentials) throws NamingException {

        boolean validated = false;

        if (userPassword == null) {
            validated = bindAsUser(context, user, credentials);
        } else {
            validated = compareCredentials(context, user, credentials);
        }

        if (containerLog.isTraceEnabled()) {
            if (validated) {
                containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", user.getUserName()));
            } else {
                containerLog.trace(sm.getString("jndiRealm.authenticateFailure", user.getUserName()));
            }
        }
        return validated;
    }


    /**
     * Check whether the credentials presented by the user match those
     * retrieved from the directory.
     *
     * @param context The directory context
     * @param info The User to be authenticated
     * @param credentials Authentication credentials
     * @return <code>true</code> if the credentials are validated
     * @exception NamingException if a directory server error occurs
     */
    protected boolean compareCredentials(DirContext context, User info, String credentials) throws NamingException {
        // Validate the credentials specified by the user
        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  validating credentials");
        }

        if (info == null || credentials == null) {
            return false;
        }

        String password = info.getPassword();

        return getCredentialHandler().matches(credentials, password);
    }


    /**
     * Check credentials by binding to the directory as the user
     *
     * @param context The directory context
     * @param user The User to be authenticated
     * @param credentials Authentication credentials
     * @return <code>true</code> if the credentials are validated
     * @exception NamingException if a directory server error occurs
     */
    protected boolean bindAsUser(DirContext context, User user, String credentials) throws NamingException {

        if (credentials == null || user == null) {
            return false;
        }

        // This is returned from the directory so will be attribute value
        // escaped if required
        String dn = user.getDN();
        if (dn == null) {
            return false;
        }

        // Validate the credentials specified by the user
        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  validating credentials by binding as the user");
        }

        userCredentialsAdd(context, dn, credentials);

        // Elicit an LDAP bind operation
        boolean validated = false;
        try {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace("  binding as "  + dn);
            }
            context.getAttributes("", null);
            validated = true;
        }
        catch (AuthenticationException e) {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace("  bind attempt failed");
            }
        }

        userCredentialsRemove(context);

        return validated;
    }


    /**
     * Configure the context to use the provided credentials for
     * authentication.
     *
     * @param context      DirContext to configure
     * @param dn           Distinguished name of user
     * @param credentials  Credentials of user
     * @exception NamingException if a directory server error occurs
     */
    private void userCredentialsAdd(DirContext context, String dn, String credentials) throws NamingException {
        // Set up security environment to bind as the user
        context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
        context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
    }


    /**
     * Configure the context to use {@link #connectionName} and
     * {@link #connectionPassword} if specified or an anonymous connection if
     * those attributes are not specified.
     *
     * @param context      DirContext to configure
     * @exception NamingException if a directory server error occurs
     */
    private void userCredentialsRemove(DirContext context) throws NamingException {
        // Restore the original security environment
        if (connectionName != null) {
            context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionName);
        } else {
            context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
        }

        if (connectionPassword != null) {
            context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword);
        } else {
            context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
        }
    }


    /**
     * Return a List of roles associated with the given User.  Any
     * roles present in the user's directory entry are supplemented by
     * a directory search. If no roles are associated with this user,
     * a zero-length List is returned.
     *
     * @param connection The directory context we are searching
     * @param user The User to be checked
     * @return the list of role names
     * @exception NamingException if a directory server error occurs
     */
    protected List<String> getRoles(JNDIConnection connection, User user) throws NamingException {

        if (user == null) {
            return null;
        }

        // This is returned from the directory so will be attribute value
        // escaped if required
        String dn = user.getDN();
        // This is the name the user provided to the authentication process so
        // it will not be escaped
        String username = user.getUserName();
        String userRoleId = user.getUserRoleId();

        if (dn == null || username == null) {
            return null;
        }

        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  getRoles(" + dn + ")");
        }

        // Start with roles retrieved from the user entry
        List<String> list = new ArrayList<>();
        List<String> userRoles = user.getRoles();
        if (userRoles != null) {
            list.addAll(userRoles);
        }
        if (commonRole != null) {
            list.add(commonRole);
        }

        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  Found " + list.size() + " user internal roles");
            containerLog.trace("  Found user internal roles " + list.toString());
        }

        // Are we configured to do role searches?
        if ((connection.roleFormat == null) || (roleName == null)) {
            return list;
        }

        // Set up parameters for an appropriate search filter
        // The dn is already attribute value escaped but the others are not
        // This is a filter so all input will require filter escaping
        String filter = connection.roleFormat.format(new String[] {
                doFilterEscaping(dn),
                doFilterEscaping(doAttributeValueEscaping(username)),
                doFilterEscaping(doAttributeValueEscaping(userRoleId)) });
        SearchControls controls = new SearchControls();
        if (roleSubtree) {
            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        } else {
            controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
        }
        controls.setReturningAttributes(new String[] {roleName});

        String base = null;
        if (connection.roleBaseFormat != null) {
            NameParser np = connection.context.getNameParser("");
            Name name = np.parse(dn);
            String nameParts[] = new String[name.size()];
            for (int i = 0; i < name.size(); i++) {
                // May have been returned with \<char> escaping rather than
                // \<hex><hex>. Make sure it is \<hex><hex>.
                nameParts[i] =  convertToHexEscape(name.get(i));
            }
            base = connection.roleBaseFormat.format(nameParts);
        } else {
            base = "";
        }

        // Perform the configured search and process the results
        NamingEnumeration<SearchResult> results = searchAsUser(connection.context, user, base, filter, controls,
                isRoleSearchAsUser());

        if (results == null) {
            return list;  // Should never happen, but just in case ...
        }

        Map<String, String> groupMap = new HashMap<>();
        try {
            while (results.hasMore()) {
                SearchResult result = results.next();
                Attributes attrs = result.getAttributes();
                if (attrs == null) {
                    continue;
                }
                String dname = getDistinguishedName(connection.context, base, result);
                String name = getAttributeValue(roleName, attrs);
                if (name != null && dname != null) {
                    groupMap.put(dname, name);
                }
            }
        } catch (PartialResultException ex) {
            if (!adCompat) {
                throw ex;
            }
        } finally {
            results.close();
        }

        if (containerLog.isTraceEnabled()) {
            Set<Entry<String, String>> entries = groupMap.entrySet();
            containerLog.trace("  Found " + entries.size() + " direct roles");
            for (Entry<String, String> entry : entries) {
                containerLog.trace(  "  Found direct role " + entry.getKey() + " -> " + entry.getValue());
            }
        }

        // if nested group search is enabled, perform searches for nested groups until no new group is found
        if (getRoleNested()) {

            // The following efficient algorithm is known as memberOf Algorithm, as described in "Practices in
            // Directory Groups". It avoids group slurping and handles cyclic group memberships as well.
            // See http://middleware.internet2.edu/dir/ for details

            Map<String, String> newGroups = new HashMap<>(groupMap);
            while (!newGroups.isEmpty()) {
                Map<String, String> newThisRound = new HashMap<>(); // Stores the groups we find in this iteration

                for (Entry<String, String> group : newGroups.entrySet()) {
                    // Group key is already value escaped if required
                    // Group value is not value escaped
                    // Everything needs to be filter escaped
                    filter = connection.roleFormat.format(new String[] {
                            doFilterEscaping(group.getKey()),
                            doFilterEscaping(doAttributeValueEscaping(group.getValue())),
                            doFilterEscaping(doAttributeValueEscaping(group.getValue())) });

                    if (containerLog.isTraceEnabled()) {
                        containerLog.trace("Perform a nested group search with base "+ roleBase +
                                " and filter " + filter);
                    }

                    results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser());

                    try {
                        while (results.hasMore()) {
                            SearchResult result = results.next();
                            Attributes attrs = result.getAttributes();
                            if (attrs == null) {
                                continue;
                            }
                            String dname = getDistinguishedName(connection.context, roleBase, result);
                            String name = getAttributeValue(roleName, attrs);
                            if (name != null && dname != null && !groupMap.keySet().contains(dname)) {
                                groupMap.put(dname, name);
                                newThisRound.put(dname, name);

                                if (containerLog.isTraceEnabled()) {
                                    containerLog.trace("  Found nested role " + dname + " -> " + name);
                                }
                            }
                        }
                    } catch (PartialResultException ex) {
                        if (!adCompat) {
                            throw ex;
                        }
                    } finally {
                        results.close();
                    }
                }

                newGroups = newThisRound;
            }
        }

        list.addAll(groupMap.values());
        return list;
    }


    /**
     * Perform the search on the context as the {@code dn}, when
     * {@code searchAsUser} is {@code true}, otherwise search the context with
     * the default credentials.
     *
     * @param context
     *            context to search on
     * @param user
     *            user to bind on
     * @param base
     *            base to start the search from
     * @param filter
     *            filter to use for the search
     * @param controls
     *            controls to use for the search
     * @param searchAsUser
     *            {@code true} when the search should be done as user, or
     *            {@code false} for using the default credentials
     * @return enumeration with all found entries
     * @throws NamingException
     *             if a directory server error occurs
     */
    private NamingEnumeration<SearchResult> searchAsUser(DirContext context, User user, String base, String filter,
            SearchControls controls, boolean searchAsUser) throws NamingException {
        NamingEnumeration<SearchResult> results;
        try {
            if (searchAsUser) {
                userCredentialsAdd(context, user.getDN(), user.getPassword());
            }
            results = context.search(base, filter, controls);
        } finally {
            if (searchAsUser) {
                userCredentialsRemove(context);
            }
        }
        return results;
    }


    /**
     * Return a String representing the value of the specified attribute.
     *
     * @param attrId Attribute name
     * @param attrs Attributes containing the required value
     * @return the attribute value
     * @exception NamingException if a directory server error occurs
     */
    private String getAttributeValue(String attrId, Attributes attrs) throws NamingException {

        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  retrieving attribute " + attrId);
        }

        if (attrId == null || attrs == null) {
            return null;
        }

        Attribute attr = attrs.get(attrId);
        if (attr == null) {
            return null;
        }
        Object value = attr.get();
        if (value == null) {
            return null;
        }
        String valueString = null;
        if (value instanceof byte[]) {
            valueString = new String((byte[]) value);
        } else {
            valueString = value.toString();
        }

        return valueString;
    }


    /**
     * Add values of a specified attribute to a list
     *
     * @param attrId Attribute name
     * @param attrs Attributes containing the new values
     * @param values ArrayList containing values found so far
     * @return the list of attribute values
     * @exception NamingException if a directory server error occurs
     */
    private ArrayList<String> addAttributeValues(String attrId, Attributes attrs, ArrayList<String> values)
            throws NamingException {

        if (containerLog.isTraceEnabled()) {
            containerLog.trace("  retrieving values for attribute " + attrId);
        }
        if (attrId == null || attrs == null) {
            return values;
        }
        if (values == null) {
            values = new ArrayList<>();
        }
        Attribute attr = attrs.get(attrId);
        if (attr == null) {
            return values;
        }
        NamingEnumeration<?> e = attr.getAll();
        try {
            while(e.hasMore()) {
                String value = (String)e.next();
                values.add(value);
            }
        } catch (PartialResultException ex) {
            if (!adCompat) {
                throw ex;
            }
        } finally {
            e.close();
        }
        return values;
    }


    /**
     * Close any open connection to the directory server for this Realm.
     *
     * @param connection The directory context to be closed
     */
    protected void close(JNDIConnection connection) {

        // Do nothing if there is no opened connection
        if (connection == null || connection.context == null) {
            if (connectionPool == null) {
                singleConnectionLock.unlock();
            }
            return;
        }

        // Close tls startResponse if used
        if (tls != null) {
            try {
                tls.close();
            } catch (IOException e) {
                containerLog.error(sm.getString("jndiRealm.tlsClose"), e);
            }
        }
        // Close our opened connection
        try {
            if (containerLog.isDebugEnabled()) {
                containerLog.debug("Closing directory context");
            }
            connection.context.close();
        } catch (NamingException e) {
            containerLog.error(sm.getString("jndiRealm.close"), e);
        }
        connection.context = null;
        // The lock will be reacquired before any manipulation of the connection
        if (connectionPool == null) {
            singleConnectionLock.unlock();
        }
    }


    /**
     * Close all pooled connections.
     */
    protected void closePooledConnections() {
        if (connectionPool != null) {
            // Close any pooled connections as they might be bad as well
            synchronized (connectionPool) {
                JNDIConnection connection = null;
                while ((connection = connectionPool.pop()) != null) {
                    close(connection);
                }
            }
        }
    }


    /**
     * Get the password for the specified user.
     * @param username The user name
     * @return the password associated with the given principal's user name.
     */
    @Override
    protected String getPassword(String username) {
        String userPassword = getUserPassword();
        if (userPassword == null || userPassword.isEmpty()) {
            return null;
        }

        JNDIConnection connection = null;
        User user = null;
        try {
            // Ensure that we have a directory context available
            connection = get();

            // Occasionally the directory context will timeout.  Try one more
            // time before giving up.
            try {
                user = getUser(connection, username, null);
            } catch (NullPointerException | NamingException e) {
                // log the exception so we know it's there.
                containerLog.info(sm.getString("jndiRealm.exception.retry"), e);

                // close the connection so we know it will be reopened.
                close(connection);
                closePooledConnections();

                // open a new directory context.
                connection = get();

                // Try the authentication again.
                user = getUser(connection, username, null);
            }

            // Release this context
            release(connection);

            if (user == null) {
                // User should be found...
                return null;
            } else {
                // ... and have a password
                return user.getPassword();
            }
        } catch (Exception e) {
            // Log the problem for posterity
            containerLog.error(sm.getString("jndiRealm.exception"), e);
            // close the connection so we know it will be reopened.
            close(connection);
            closePooledConnections();
            return null;
        }
    }


    /**
     * Get the principal associated with the specified certificate.
     * @param username The user name
     * @return the Principal associated with the given certificate.
     */
    @Override
    protected Principal getPrincipal(String username) {
        return getPrincipal(username, null);
    }


    @Override
    protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
        String name = gssName.toString();

        if (isStripRealmForGss()) {
            int i = name.indexOf('@');
            if (i > 0) {
                // Zero so we don't leave a zero length name
                name = name.substring(0, i);
            }
        }

        return getPrincipal(name, gssCredential);
    }


    protected Principal getPrincipal(String username, GSSCredential gssCredential) {

        JNDIConnection connection = null;
        Principal principal = null;

        try {
            // Ensure that we have a directory context available
            connection = get();

            // Occasionally the directory context will timeout.  Try one more
            // time before giving up.
            try {

                // Authenticate the specified username if possible
                principal = getPrincipal(connection, username, gssCredential);

            } catch (CommunicationException | ServiceUnavailableException e) {
                // log the exception so we know it's there.
                containerLog.info(sm.getString("jndiRealm.exception.retry"), e);

                // close the connection so we know it will be reopened.
                close(connection);
                closePooledConnections();

                // open a new directory context.
                connection = get();

                // Try the authentication again.
                principal = getPrincipal(connection, username, gssCredential);
            }

            // Release this context
            release(connection);

            // Return the authenticated Principal (if any)
            return principal;

        } catch (Exception e) {
            // Log the problem for posterity
            containerLog.error(sm.getString("jndiRealm.exception"), e);

            // close the connection so we know it will be reopened.
            close(connection);
            closePooledConnections();

            // Return "not authenticated" for this request
            return null;
        }
    }


    /**
     * Get the principal associated with the specified certificate.
     * @param connection The directory context
     * @param username The user name
     * @param gssCredential The credentials
     * @return the Principal associated with the given certificate.
     * @exception NamingException if a directory server error occurs
     */
    protected Principal getPrincipal(JNDIConnection connection, String username, GSSCredential gssCredential)
            throws NamingException {

        User user = null;
        List<String> roles = null;
        Hashtable<?, ?> preservedEnvironment = null;
        DirContext context = connection.context;

        try {
            if (gssCredential != null && isUseDelegatedCredential()) {
                // Preserve the current context environment parameters
                preservedEnvironment = context.getEnvironment();
                // Set up context
                context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "GSSAPI");
                context.addToEnvironment("javax.security.sasl.server.authentication", "true");
                context.addToEnvironment("javax.security.sasl.qop", spnegoDelegationQop);
                // Note: Subject already set in SPNEGO authenticator so no need
                //       for Subject.doAs() here
            }
            user = getUser(connection, username);
            if (user != null) {
                roles = getRoles(connection, user);
            }
        } finally {
            if (gssCredential != null && isUseDelegatedCredential()) {
                restoreEnvironmentParameter(context, Context.SECURITY_AUTHENTICATION, preservedEnvironment);
                restoreEnvironmentParameter(context, "javax.security.sasl.server.authentication", preservedEnvironment);
                restoreEnvironmentParameter(context, "javax.security.sasl.qop", preservedEnvironment);
            }
        }

        if (user != null) {
            return new GenericPrincipal(user.getUserName(), roles, null, null, gssCredential);
        }

        return null;
    }


    private void restoreEnvironmentParameter(DirContext context,
            String parameterName, Hashtable<?, ?> preservedEnvironment) {
        try {
            context.removeFromEnvironment(parameterName);
            if (preservedEnvironment != null && preservedEnvironment.containsKey(parameterName)) {
                context.addToEnvironment(parameterName,
                        preservedEnvironment.get(parameterName));
            }
        } catch (NamingException e) {
            // Ignore
        }
    }


    /**
     * Open (if necessary) and return a connection to the configured
     * directory server for this Realm.
     * @return the connection
     * @exception NamingException if a directory server error occurs
     */
    protected JNDIConnection get() throws NamingException {
        JNDIConnection connection = null;
        // Use the pool if available, otherwise use the single connection
        if (connectionPool != null) {
            connection = connectionPool.pop();
            if (connection == null) {
                connection = create();
            }
        } else {
            singleConnectionLock.lock();
            if (singleConnection == null) {
                singleConnection = create();
            }
            connection = singleConnection;
        }
        if (connection.context == null) {
            open(connection);
        }
        return connection;
    }


    /**
     * Release our use of this connection so that it can be recycled.
     *
     * @param connection The directory context to release
     */
    protected void release(JNDIConnection connection) {
        if (connectionPool != null) {
            if (connection != null) {
                if (!connectionPool.push(connection)) {
                    // Any connection that doesn't end back to the pool must be closed
                    close(connection);
                }
            }
        } else {
            singleConnectionLock.unlock();
        }
    }


    /**
     * Create a new connection wrapper, along with the
     * message formats.
     * @return the new connection
     */
    protected JNDIConnection create() {
        return new JNDIConnection(userSearch, userPatternArray, roleBase, roleSearch);
    }


    /**
     * Create a new connection to the directory server.
     * @param connection The directory server connection wrapper
     * @throws NamingException if a directory server error occurs
     */
    protected void open(JNDIConnection connection) throws NamingException {
        try {
            // Ensure that we have a directory context available
            connection.context = createDirContext(getDirectoryContextEnvironment());
        } catch (Exception e) {
            if (alternateURL == null || alternateURL.length() == 0) {
                // No alternate URL. Re-throw the exception.
                throw e;
            }
            connectionAttempt = 1;
            // log the first exception.
            containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
            // Try connecting to the alternate url.
            connection.context = createDirContext(getDirectoryContextEnvironment());
        } finally {
            // reset it in case the connection times out.
            // the primary may come back.
            connectionAttempt = 0;
        }
    }


    @Override
    public boolean isAvailable() {
        // Simple best effort check
        return (connectionPool != null || singleConnection.context != null);
    }


    private DirContext createDirContext(Hashtable<String, String> env) throws NamingException {
        if (useStartTls) {
            return createTlsDirContext(env);
        } else {
            return new InitialDirContext(env);
        }
    }


    private SSLSocketFactory getSSLSocketFactory() {
        if (sslSocketFactory != null) {
            return sslSocketFactory;
        }
        final SSLSocketFactory result;
        if (this.sslSocketFactoryClassName != null && !sslSocketFactoryClassName.trim().equals("")) {
            result = createSSLSocketFactoryFromClassName(this.sslSocketFactoryClassName);
        } else {
            result = createSSLContextFactoryFromProtocol(sslProtocol);
        }
        this.sslSocketFactory = result;
        return result;
    }


    private SSLSocketFactory createSSLSocketFactoryFromClassName(String className) {
        try {
            Object o = constructInstance(className);
            if (o instanceof SSLSocketFactory) {
                return sslSocketFactory;
            } else {
                throw new IllegalArgumentException(sm.getString(
                        "jndiRealm.invalidSslSocketFactory",
                        className));
            }
        } catch (ReflectiveOperationException | SecurityException e) {
            throw new IllegalArgumentException(sm.getString(
                    "jndiRealm.invalidSslSocketFactory",
                    className), e);
        }
    }


    private SSLSocketFactory createSSLContextFactoryFromProtocol(String protocol) {
        try {
            SSLContext sslContext;
            if (protocol != null) {
                sslContext = SSLContext.getInstance(protocol);
                sslContext.init(null, null, null);
            } else {
                sslContext = SSLContext.getDefault();
            }
            return sslContext.getSocketFactory();
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            List<String> allowedProtocols = Arrays.asList(getSupportedSslProtocols());
            throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslProtocol",
                    protocol, allowedProtocols), e);
        }
    }


    /**
     * Create a tls enabled LdapContext and set the StartTlsResponse tls
     * instance variable.
     *
     * @param env
     *            Environment to use for context creation
     * @return configured {@link LdapContext}
     * @throws NamingException
     *             when something goes wrong while negotiating the connection
     */
    private DirContext createTlsDirContext(Hashtable<String, String> env) throws NamingException {
        Map<String, Object> savedEnv = new HashMap<>();
        for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, Context.SECURITY_CREDENTIALS,
                Context.SECURITY_PRINCIPAL, Context.SECURITY_PROTOCOL)) {
            Object entry = env.remove(key);
            if (entry != null) {
                savedEnv.put(key, entry);
            }
        }
        LdapContext result = null;
        try {
            result = new InitialLdapContext(env, null);
            tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest());
            if (getHostnameVerifier() != null) {
                tls.setHostnameVerifier(getHostnameVerifier());
            }
            if (getCipherSuitesArray() != null) {
                tls.setEnabledCipherSuites(getCipherSuitesArray());
            }
            try {
                SSLSession negotiate = tls.negotiate(getSSLSocketFactory());
                containerLog.debug(sm.getString("jndiRealm.negotiatedTls", negotiate.getProtocol()));
            } catch (IOException e) {
                throw new NamingException(e.getMessage());
            }
        } finally {
            if (result != null) {
                for (Map.Entry<String, Object> savedEntry : savedEnv.entrySet()) {
                    result.addToEnvironment(savedEntry.getKey(), savedEntry.getValue());
                }
            }
        }
        return result;
    }


    /**
     * Create our directory context configuration.
     *
     * @return java.util.Hashtable the configuration for the directory context.
     */
    protected Hashtable<String,String> getDirectoryContextEnvironment() {

        Hashtable<String,String> env = new Hashtable<>();

        // Configure our directory context environment.
        if (containerLog.isDebugEnabled() && connectionAttempt == 0) {
            containerLog.debug("Connecting to URL " + connectionURL);
        } else if (containerLog.isDebugEnabled() && connectionAttempt > 0) {
            containerLog.debug("Connecting to URL " + alternateURL);
        }
        env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
        if (connectionName != null) {
            env.put(Context.SECURITY_PRINCIPAL, connectionName);
        }
        if (connectionPassword != null) {
            env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
        }
        if (connectionURL != null && connectionAttempt == 0) {
            env.put(Context.PROVIDER_URL, connectionURL);
        } else if (alternateURL != null && connectionAttempt > 0) {
            env.put(Context.PROVIDER_URL, alternateURL);
        }
        if (authentication != null) {
            env.put(Context.SECURITY_AUTHENTICATION, authentication);
        }
        if (protocol != null) {
            env.put(Context.SECURITY_PROTOCOL, protocol);
        }
        if (referrals != null) {
            env.put(Context.REFERRAL, referrals);
        }
        if (derefAliases != null) {
            env.put(JNDIRealm.DEREF_ALIASES, derefAliases);
        }
        if (connectionTimeout != null) {
            env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
        }
        if (readTimeout != null) {
            env.put("com.sun.jndi.ldap.read.timeout", readTimeout);
        }

        return env;
    }


    // ------------------------------------------------------ Lifecycle Methods

    /**
     * Prepare for the beginning of active use of the public methods of this
     * component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    @Override
    protected void startInternal() throws LifecycleException {

        if (connectionPoolSize != 1) {
            connectionPool = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, connectionPoolSize);
        }

        // Check to see if the connection to the directory can be opened
        ClassLoader ocl = null;
        JNDIConnection connection = null;
        try {
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=65553
            // This can move back to open() once it is known that Tomcat must be
            // running on a JVM that includes a fix for
            // https://bugs.openjdk.java.net/browse/JDK-8273874
            if (!isUseContextClassLoader()) {
                ocl = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
            }
            connection = get();
        } catch (NamingException e) {
            // A failure here is not fatal as the directory may be unavailable
            // now but available later. Unavailability of the directory is not
            // fatal once the Realm has started so there is no reason for it to
            // be fatal when the Realm starts.
            containerLog.error(sm.getString("jndiRealm.open"), e);
        } finally {
            release(connection);
            if (!isUseContextClassLoader()) {
                Thread.currentThread().setContextClassLoader(ocl);
            }
        }

        super.startInternal();
    }


    /**
     * Gracefully terminate the active use of the public methods of this
     * component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that needs to be reported
     */
    @Override
    protected void stopInternal() throws LifecycleException {
        super.stopInternal();
        // Close any open directory server connection
        if (connectionPool == null) {
            singleConnectionLock.lock();
            close(singleConnection);
        } else {
            closePooledConnections();
            connectionPool = null;
        }
    }


    /**
     * Given a string containing LDAP patterns for user locations (separated by
     * parentheses in a pseudo-LDAP search string format -
     * "(location1)(location2)", returns an array of those paths.  Real LDAP
     * search strings are supported as well (though only the "|" "OR" type).
     *
     * @param userPatternString - a string LDAP search paths surrounded by
     * parentheses
     * @return a parsed string array
     */
    protected String[] parseUserPatternString(String userPatternString) {

        if (userPatternString != null) {
            List<String> pathList = new ArrayList<>();
            int startParenLoc = userPatternString.indexOf('(');
            if (startParenLoc == -1) {
                // no parens here; return whole thing
                return new String[] {userPatternString};
            }
            int startingPoint = 0;
            while (startParenLoc > -1) {
                int endParenLoc = 0;
                // weed out escaped open parens and parens enclosing the
                // whole statement (in the case of valid LDAP search
                // strings: (|(something)(somethingelse))
                while ( (userPatternString.charAt(startParenLoc + 1) == '|') ||
                        (startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\') ) {
                    startParenLoc = userPatternString.indexOf('(', startParenLoc+1);
                }
                endParenLoc = userPatternString.indexOf(')', startParenLoc+1);
                // weed out escaped end-parens
                while (userPatternString.charAt(endParenLoc - 1) == '\\') {
                    endParenLoc = userPatternString.indexOf(')', endParenLoc+1);
                }
                String nextPathPart = userPatternString.substring(startParenLoc+1, endParenLoc);
                pathList.add(nextPathPart);
                startingPoint = endParenLoc+1;
                startParenLoc = userPatternString.indexOf('(', startingPoint);
            }
            return pathList.toArray(new String[] {});
        }
        return null;
    }


    /**
     * Given an LDAP search string, returns the string with certain characters
     * escaped according to RFC 2254 guidelines.
     * The character mapping is as follows:
     *     char -&gt;  Replacement
     *    ---------------------------
     *     *  -&gt; \2a
     *     (  -&gt; \28
     *     )  -&gt; \29
     *     \  -&gt; \5c
     *     \0 -&gt; \00
     *
     * @param inString string to escape according to RFC 2254 guidelines
     *
     * @return String the escaped/encoded result
     */
    protected String doFilterEscaping(String inString) {
        if (inString == null) {
            return null;
        }
        StringBuilder buf = new StringBuilder(inString.length());
        for (int i = 0; i < inString.length(); i++) {
            char c = inString.charAt(i);
            switch (c) {
                case '\\':
                    buf.append("\\5c");
                    break;
                case '*':
                    buf.append("\\2a");
                    break;
                case '(':
                    buf.append("\\28");
                    break;
                case ')':
                    buf.append("\\29");
                    break;
                case '\0':
                    buf.append("\\00");
                    break;
                default:
                    buf.append(c);
                    break;
            }
        }
        return buf.toString();
    }


    /**
     * Returns the distinguished name of a search result.
     *
     * @param context Our DirContext
     * @param base The base DN
     * @param result The search result
     * @return String containing the distinguished name
     * @exception NamingException if a directory server error occurs
     */
    protected String getDistinguishedName(DirContext context, String base, SearchResult result) throws NamingException {
        // Get the entry's distinguished name.  For relative results, this means
        // we need to composite a name with the base name, the context name, and
        // the result name.  For non-relative names, use the returned name.
        String resultName = result.getName();
        Name name;
        if (result.isRelative()) {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace("  search returned relative name: " + resultName);
            }
            NameParser parser = context.getNameParser("");
            Name contextName = parser.parse(context.getNameInNamespace());
            Name baseName = parser.parse(base);

            // Bugzilla 32269
            Name entryName = parser.parse(new CompositeName(resultName).get(0));

            name = contextName.addAll(baseName);
            name = name.addAll(entryName);
        } else {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace("  search returned absolute name: " + resultName);
            }
            try {
                // Normalize the name by running it through the name parser.
                NameParser parser = context.getNameParser("");
                URI userNameUri = new URI(resultName);
                String pathComponent = userNameUri.getPath();
                // Should not ever have an empty path component, since that is /{DN}
                if (pathComponent.length() < 1 ) {
                    throw new InvalidNameException("Search returned unparseable absolute name: " + resultName);
                }
                name = parser.parse(pathComponent.substring(1));
            } catch ( URISyntaxException e ) {
                throw new InvalidNameException("Search returned unparseable absolute name: " + resultName);
            }
        }

        if (getForceDnHexEscape()) {
            // Bug 63026
            return convertToHexEscape(name.toString());
        } else {
            return name.toString();
        }
    }


    /**
     * Implements the necessary escaping to represent an attribute value as a
     * String as per RFC 4514.
     *
     * @param input The original attribute value
     * @return      The string representation of the attribute value
     */
    protected String doAttributeValueEscaping(String input) {
        if (input == null) {
            return null;
        }
        int len = input.length();
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < len; i++) {
            char c = input.charAt(i);
            switch (c) {
                case ' ': {
                    if (i == 0 || i == (len -1)) {
                        result.append("\\20");
                    } else {
                        result.append(c);
                    }
                    break;
                }
                case '#': {
                    if (i == 0 ) {
                        result.append("\\23");
                    } else {
                        result.append(c);
                    }
                    break;
                }
                case '\"': {
                    result.append("\\22");
                    break;
                }
                case '+': {
                    result.append("\\2B");
                    break;
                }
                case ',': {
                    result.append("\\2C");
                    break;
                }
                case ';': {
                    result.append("\\3B");
                    break;
                }
                case '<': {
                    result.append("\\3C");
                    break;
                }
                case '>': {
                    result.append("\\3E");
                    break;
                }
                case '\\': {
                    result.append("\\5C");
                    break;
                }
                case '\u0000': {
                    result.append("\\00");
                    break;
                }
                default:
                    result.append(c);
            }

        }

        return result.toString();
    }


    protected static String convertToHexEscape(String input) {
        if (input.indexOf('\\') == -1) {
            // No escaping present. Return original.
            return input;
        }

        // +6 allows for 3 escaped characters by default
        StringBuilder result = new StringBuilder(input.length() + 6);
        boolean previousSlash = false;
        for (int i = 0; i < input.length(); i++) {
            char c = input.charAt(i);

            if (previousSlash) {
                switch (c) {
                    case ' ': {
                        result.append("\\20");
                        break;
                    }
                    case '\"': {
                        result.append("\\22");
                        break;
                    }
                    case '#': {
                        result.append("\\23");
                        break;
                    }
                    case '+': {
                        result.append("\\2B");
                        break;
                    }
                    case ',': {
                        result.append("\\2C");
                        break;
                    }
                    case ';': {
                        result.append("\\3B");
                        break;
                    }
                    case '<': {
                        result.append("\\3C");
                        break;
                    }
                    case '=': {
                        result.append("\\3D");
                        break;
                    }
                    case '>': {
                        result.append("\\3E");
                        break;
                    }
                    case '\\': {
                        result.append("\\5C");
                        break;
                    }
                    default:
                        result.append('\\');
                        result.append(c);
                }
                previousSlash = false;
            } else {
                if (c == '\\') {
                    previousSlash = true;
                } else {
                    result.append(c);
                }
            }
        }

        if (previousSlash) {
            result.append('\\');
        }

        return result.toString();
    }


    // ------------------------------------------------------ Protected Classes

    /**
     * A protected class representing a User
     */
    protected static class User {

        private final String username;
        private final String dn;
        private final String password;
        private final List<String> roles;
        private final String userRoleId;

        public User(String username, String dn, String password, List<String> roles, String userRoleId) {
            this.username = username;
            this.dn = dn;
            this.password = password;
            if (roles == null) {
                this.roles = Collections.emptyList();
            } else {
                this.roles = Collections.unmodifiableList(roles);
            }
            this.userRoleId = userRoleId;
        }

        public String getUserName() {
            return username;
        }

        public String getDN() {
            return dn;
        }

        public String getPassword() {
            return password;
        }

        public List<String> getRoles() {
            return roles;
        }

        public String getUserRoleId() {
            return userRoleId;
        }
    }


    /**
     * Class holding the connection to the directory plus the associated
     * non thread safe message formats.
     */
    protected static class JNDIConnection {

        /**
         * The MessageFormat object associated with the current
         * <code>userSearch</code>.
         */
        public final MessageFormat userSearchFormat;

        /**
         * An array of MessageFormat objects associated with the current
         * <code>userPatternArray</code>.
         */
        public final MessageFormat[] userPatternFormatArray;

        /**
         * The MessageFormat object associated with the current
         * <code>roleBase</code>.
         */
        public final MessageFormat roleBaseFormat;

        /**
         * The MessageFormat object associated with the current
         * <code>roleSearch</code>.
         */
        public final MessageFormat roleFormat ;

        /**
         * The directory context linking us to our directory server.
         */
        public volatile DirContext context = null;


        public JNDIConnection(String userSearch, String[] userPatternArray, String roleBase, String roleSearch) {
            if (userSearch == null) {
                userSearchFormat = null;
            } else {
                userSearchFormat = new MessageFormat(userSearch);
            }

            if (userPatternArray == null) {
                userPatternFormatArray = null;
            } else {
                int len = userPatternArray.length;
                userPatternFormatArray = new MessageFormat[len];
                for (int i = 0; i < len; i++) {
                    userPatternFormatArray[i] = new MessageFormat(userPatternArray[i]);
                }
            }

            if (roleBase == null) {
                roleBaseFormat = null;
            } else {
                roleBaseFormat = new MessageFormat(roleBase);
            }

            if (roleSearch == null) {
                roleFormat = null;
            } else {
                roleFormat = new MessageFormat(roleSearch);
            }
        }
    }
}
