/*
 *   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
 *
 *     https://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.directory.fortress.core.ldap;


import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyRequest;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyRequestImpl;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponse;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.SearchCursor;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
import org.apache.directory.api.ldap.model.message.*;
import org.apache.directory.api.ldap.model.message.controls.ProxiedAuthz;
import org.apache.directory.api.ldap.model.message.controls.ProxiedAuthzImpl;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.extras.controls.relax.RelaxControlImpl;

import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.model.Constraint;
import org.apache.directory.fortress.core.model.ConstraintUtil;
import org.apache.directory.fortress.core.model.FortEntity;
import org.apache.directory.fortress.core.model.Hier;
import org.apache.directory.fortress.core.model.Relationship;
import org.apache.directory.fortress.core.util.Config;
import org.apache.directory.fortress.core.util.LdapUtil;
import org.apache.directory.ldap.client.api.LdapConnection;


/**
 * Abstract class contains methods to perform low-level entity to ldap persistence.  These methods are called by the
 * Fortress DAO's, i.e. {@link org.apache.directory.fortress.core.impl.UserDAO}. {@link org.apache.directory.fortress.core.impl.RoleDAO},
 * {@link org.apache.directory.fortress.core.impl.PermDAO}, ....
 * These are low-level data utilities, very little if any data validations are performed here.
 * <p>
 * This class is thread safe.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public abstract class LdapDataProvider
{
    // Logging
    private static final String CLS_NM = LdapDataProvider.class.getName();
    private static final int MAX_DEPTH = 100;
    private static final LdapCounters COUNTERS = new LdapCounters();
    private static final PasswordPolicyRequest PP_REQ_CTRL = new PasswordPolicyRequestImpl();

    /**
     * Given a contextId and a fortress param name return the LDAP dn.
     *
     * @param contextId is to determine what sub-tree to use.
     * @param root      contains the fortress parameter name that corresponds with a particular LDAP container.
     * @return String contains the dn to use for operation.
     */
    protected static String getRootDn( String contextId, String root )
    {
        String szDn = Config.getInstance().getProperty( root );

        // The contextId must not be null, or "HOME" or "null"
        if ( StringUtils.isNotEmpty( contextId ) && !contextId.equalsIgnoreCase( GlobalIds.NULL ) && !contextId
            .equals( GlobalIds.HOME ) )
        {
            int idx = szDn.indexOf( Config.getInstance().getProperty( GlobalIds.SUFFIX ) );
            if ( idx > 0 )
            {
                // Found. The DN is ,ou=<contextId>,
                StringBuilder dn = new StringBuilder();
                dn.append( szDn.substring( 0, idx - 1 ) ).append( "," ).append( SchemaConstants.OU_AT ).append( "=" )
                    .append(
                        contextId ).append( "," ).append( szDn.substring( idx ) );

                return dn.toString();
            }
            else
            {
                return "";
            }
        }
        else
        {
            return szDn;
        }
    }

    /**
     * Given a contextId return the LDAP dn that includes the suffix.
     *
     * @param contextId is to determine what sub-tree to use.
     * @return String contains the dn to use for operation.
     */
    protected String getRootDn( String contextId )
    {
        StringBuilder dn = new StringBuilder();
        if ( StringUtils.isNotEmpty( contextId ) && !contextId.equalsIgnoreCase( GlobalIds.NULL ) && !contextId
            .equals( GlobalIds.HOME ) )
        {
            dn.append( SchemaConstants.OU_AT ).append( "=" ).append( contextId ).append( "," +
                "" ).append( Config.getInstance().getProperty( GlobalIds.SUFFIX ) );
        }
        else
        {
            dn.append( Config.getInstance().getProperty( GlobalIds.SUFFIX ) );
        }
        return dn.toString();
    }


    /**
     * Read the ldap record from specified location.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains ldap distinguished name.
     * @param attrs      array contains array names to pull back.
     * @return ldap entry.
     * @throws LdapException in the event system error occurs.
     */
    protected Entry read( LdapConnection connection, String dn, String[] attrs ) throws LdapException
    {
        COUNTERS.incrementRead();

        return connection.lookup( dn, attrs );
    }


    /**
     * Read the ldap record from specified location.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains ldap distinguished name.
     * @param attrs      array contains array names to pull back.
     * @return ldap entry.
     * @throws LdapException in the event system error occurs.
     */
    protected Entry read( LdapConnection connection, Dn dn, String[] attrs ) throws LdapException
    {
        COUNTERS.incrementRead();

        return connection.lookup( dn, attrs );
    }


    /**
     * Read the ldap record from specified location with user assertion.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains ldap distinguished name.
     * @param attrs      array contains array names to pull back.                                        ,
     *                   PoolMgr.ConnType.USER
     * @param userDn     string value represents the identity of user on who's behalf the request was initiated.  The
     *                   value will be stored in openldap auditsearch record AuthZID's attribute.
     * @return ldap entry.
     * @throws LdapException                in the event system error occurs.
     * @throws UnsupportedEncodingException for search control errors.
     */
    protected Entry read( LdapConnection connection, String dn, String[] attrs, String userDn ) throws LdapException
    {
        COUNTERS.incrementRead();

        return connection.lookup( dn, attrs );
    }


    /**
     * Add a new ldap entry to the directory.  Do not add audit context.
     *
     * @param connection handle to ldap connection.
     * @param entry      contains data to add..
     * @throws LdapException in the event system error occurs.
     */
    protected void add( LdapConnection connection, Entry entry ) throws LdapException
    {
        COUNTERS.incrementAdd();
        connection.add( entry );
    }


    /**
     * Add a new ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param entry      contains data to add..
     * @param entity     contains audit context.
     * @throws LdapException in the event system error occurs.
     */
    protected void add( LdapConnection connection, Entry entry, FortEntity entity ) throws LdapException
    {
        add( connection, entry, entity, false );
    }


    /**
     * Add a new ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param entry      contains data to add..
     * @param entity     contains audit context.
     * @param setRelaxControl   when true attaches relax control to request
     * @throws LdapException in the event system error occurs.
     */
    protected void add( LdapConnection connection, Entry entry, FortEntity entity, boolean setRelaxControl ) throws LdapException
    {
        COUNTERS.incrementAdd();

        if ( Config.getInstance().isAuditEnabled() && ( entity != null ) && ( entity.getAdminSession() != null ) )
        {
            if ( StringUtils.isNotEmpty( entity.getAdminSession().getInternalUserId() ) )
            {
                entry.add( GlobalIds.FT_MODIFIER, entity.getAdminSession().getInternalUserId() );
            }

            if ( StringUtils.isNotEmpty( entity.getModCode() ) )
            {
                entry.add( GlobalIds.FT_MODIFIER_CODE, entity.getModCode() );
            }

            if ( StringUtils.isNotEmpty( entity.getModId() ) )
            {
                entry.add( GlobalIds.FT_MODIFIER_ID, entity.getModId() );
            }
        }
        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( entry );
        if ( setRelaxControl )
        {
            addRequest.addControl( new RelaxControlImpl() );
        }
        AddResponse response = connection.add( addRequest );
        ResultCodeEnum.processResponse(response);
    }


    /**
     * Update exiting ldap entry to the directory.  Do not add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, String dn, List<Modification> mods ) throws LdapException
    {
        COUNTERS.incrementMod();
        connection.modify( dn, mods.toArray( new Modification[]{} ) );
    }


    /**
     * Update exiting ldap entry to the directory.  Do not add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, Dn dn, List<Modification> mods ) throws LdapException
    {
        COUNTERS.incrementMod();
        connection.modify( dn, mods.toArray( new Modification[]
            {} ) );
    }


    /**
     * Update exiting ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @param entity     contains audit context.
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, String dn, List<Modification> mods,
        FortEntity entity ) throws LdapException
    {
        modify( connection, dn, mods, entity, false );
    }


    /**
     * Update exiting ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @param entity     contains audit context.
     * @param setRelaxControl   when true attaches relax control to request
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, String dn, List<Modification> mods,
        FortEntity entity, boolean setRelaxControl ) throws LdapException
    {
        Control setControl = null;
        if ( setRelaxControl )
        {
            setControl = new RelaxControlImpl();
        }
        modify( connection, dn, mods, entity, setControl );
    }


    /**
     * Update exiting ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @param entity     contains audit context.
     * @param setControl add specified control to the request
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, String dn, List<Modification> mods,
        FortEntity entity, Control setControl ) throws LdapException
    {
        COUNTERS.incrementMod();
        audit( mods, entity );
        ModifyRequest modRequest = new ModifyRequestImpl();
        // TODO: find a better way:
        for( Modification mod : mods )
        {
            modRequest.addModification( mod );
        }
        if ( setControl != null )
        {
            modRequest.addControl( setControl );
        }
        modRequest.setName( new Dn( dn ) );
        ModifyResponse response = connection.modify( modRequest );
        ResultCodeEnum.processResponse(response);
    }


    /**
     * Update exiting ldap entry to the directory.  Add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry.
     * @param mods       contains data to modify.
     * @param entity     contains audit context.
     * @throws LdapException in the event system error occurs.
     */
    protected void modify( LdapConnection connection, Dn dn, List<Modification> mods,
        FortEntity entity ) throws LdapException
    {
        COUNTERS.incrementMod();
        audit( mods, entity );
        connection.modify( dn, mods.toArray( new Modification[] {} ) );
    }


    /**
     * Delete exiting ldap entry from the directory.  Do not add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry targeted for removal..
     * @throws LdapException in the event system error occurs.
     */
    protected void delete( LdapConnection connection, String dn ) throws LdapException
    {
        COUNTERS.incrementDelete();
        connection.delete( dn );
    }


    /**
     * Delete exiting ldap entry from the directory.  Add audit context.  This method will call modify prior to
     * delete which will
     * force corresponding audit record to be written to slapd access log.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry targeted for removal..
     * @param entity     contains audit context.
     * @throws LdapException in the event system error occurs.
     */
    protected void delete( LdapConnection connection, String dn, FortEntity entity ) throws LdapException
    {
        COUNTERS.incrementDelete();
        List<Modification> mods = new ArrayList<Modification>();
        audit( mods, entity );

        if ( mods.size() > 0 )
        {
            modify( connection, dn, mods );
        }

        connection.delete( dn );
    }


    /**
     * Delete exiting ldap entry from the directory.  Add audit context.  This method will call modify prior to
     * delete which will
     * force corresponding audit record to be written to slapd access log.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry targeted for removal..
     * @param entity     contains audit context.
     * @throws LdapException in the event system error occurs.
     */
    protected void delete( LdapConnection connection, Dn dn, FortEntity entity ) throws LdapException
    {
        COUNTERS.incrementDelete();
        List<Modification> mods = new ArrayList<Modification>();
        audit( mods, entity );

        if ( mods.size() > 0 )
        {
            modify( connection, dn, mods );
        }

        connection.delete( dn );
    }


    /**
     * Delete exiting ldap entry and all descendants from the directory.  Do not add audit context.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry targeted for removal..
     * @throws LdapException   in the event system error occurs.
     * @throws IOException
     * @throws CursorException
     */
    protected void deleteRecursive( LdapConnection connection, String dn ) throws LdapException, CursorException
    {
        int recursiveCount = 0;
        deleteRecursive( dn, connection, recursiveCount );
    }


    /**
     * Delete exiting ldap entry and all descendants from the directory.  Add audit context.  This method will call
     * modify prior to delete which will
     * force corresponding audit record to be written to slapd access log.
     *
     * @param connection handle to ldap connection.
     * @param dn         contains distinguished node of entry targeted for removal..
     * @param entity     contains audit context.
     * @throws LdapException   in the event system error occurs.
     * @throws CursorException
     */
    protected void deleteRecursive( LdapConnection connection, String dn, FortEntity entity ) throws LdapException,
        CursorException
    {
        List<Modification> mods = new ArrayList<Modification>();
        audit( mods, entity );

        if ( mods.size() > 0 )
        {
            modify( connection, dn, mods );
        }

        deleteRecursive( connection, dn );
    }


    /**
     * Used to recursively remove all nodes up to record pointed to by dn attribute.
     *
     * @param dn             contains distinguished node of entry targeted for removal..
     * @param connection     handle to ldap connection.
     * @param recursiveCount keeps track of how many iterations have been performed.
     * @throws LdapException   in the event system error occurs.
     * @throws CursorException
     */
    private void deleteRecursive( String dn, LdapConnection connection, int recursiveCount ) throws LdapException,
        CursorException
    {
        String method = "deleteRecursive";

        // Sanity check - only allow max tree depth of 100
        if ( recursiveCount++ > MAX_DEPTH )
        {
            // too deep inside of a recursive sequence;
            String error = "." + method + " dn [" + dn + "] depth error in recursive";
            throw new LdapOperationErrorException( error );
        }

        String theDN;

        // Find child nodes
        SearchCursor cursor = search( connection, dn, SearchScope.ONELEVEL, "(objectclass=*)",
            SchemaConstants.NO_ATTRIBUTE_ARRAY,
            false, 0 );

        // Iterate over all entries under this entry
        while ( cursor.next() )
        {
            try
            {
                // Next directory entry
                Entry entry = cursor.getEntry();
                theDN = entry.getDn().getName();
                // continue down:
                deleteRecursive( theDN, connection, recursiveCount );
                recursiveCount--;
            }
            catch ( LdapException le )
            {
                // cannot continue;
                String error = "." + method + " dn [" + dn + "] caught LdapException=" + le.getMessage();
                throw new LdapException( error );
            }
        }

        // delete the node:
        COUNTERS.incrementDelete();
        delete( connection, dn );
    }


    /**
     * Add the audit context variables to the modfication set.
     *
     * @param mods   used to update ldap attributes.
     * @param entity contains audit context.
     */
    private void audit( List<Modification> mods, FortEntity entity )
    {
        if ( Config.getInstance().isAuditEnabled() && ( entity != null ) && ( entity.getAdminSession() != null ) )
        {
            if ( StringUtils.isNotEmpty( entity.getAdminSession().getInternalUserId() ) )
            {
                Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                    GlobalIds.FT_MODIFIER, entity.getAdminSession().getInternalUserId() );
                mods.add( modification );
            }

            if ( StringUtils.isNotEmpty( entity.getModCode() ) )
            {
                Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                    GlobalIds.FT_MODIFIER_CODE, entity.getModCode() );
                mods.add( modification );
            }

            if ( StringUtils.isNotEmpty( entity.getModId() ) )
            {
                Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                    GlobalIds.FT_MODIFIER_ID, entity.getModId() );
                mods.add( modification );
            }
        }
    }


    /**
     * Perform normal ldap search accepting default batch size.
     *
     * @param connection is LdapConnection object used for all communication with host.
     * @param baseDn     contains address of distinguished name to begin ldap search
     * @param scope      indicates depth of search starting at basedn.  0 (base dn),
     *                   1 (one level down) or 2 (infinite) are valid values.
     * @param filter     contains the search criteria
     * @param attrs      is the requested list of attritubutes to return from directory search.
     * @param attrsOnly  if true pull back attribute names only.
     * @return result set containing ldap entries returned from directory.
     * @throws LdapException thrown in the event of error in ldap client or server code.
     */
    protected SearchCursor search( LdapConnection connection, String baseDn, SearchScope scope, String filter,
        String[] attrs, boolean attrsOnly ) throws LdapException
    {
        COUNTERS.incrementSearch();

        SearchRequest searchRequest = new SearchRequestImpl();
        searchRequest.setBase( new Dn( baseDn ) );
        searchRequest.setScope( scope );
        searchRequest.setFilter( filter );
        searchRequest.setTypesOnly( attrsOnly );
        searchRequest.addAttributes( attrs );

        return connection.search( searchRequest );
    }


    /**
     * Perform normal ldap search specifying default batch size and max entries to return.
     *
     * @param connection is LdapConnection object used for all communication with host.
     * @param baseDn     contains address of distinguished name to begin ldap search
     * @param scope      indicates depth of search starting at basedn.  0 (base dn),
     *                   1 (one level down) or 2 (infinite) are valid values.
     * @param filter     contains the search criteria
     * @param attrs      is the requested list of attritubutes to return from directory search.
     * @param attrsOnly  if true pull back attribute names only.
     * @param maxEntries specifies the maximum number of entries to return in this search query.
     * @return result set containing ldap entries returned from directory.
     * @throws LdapException thrown in the event of error in ldap client or server code.
     */
    protected SearchCursor search( LdapConnection connection, String baseDn, SearchScope scope, String filter,
        String[] attrs, boolean attrsOnly, int maxEntries ) throws LdapException
    {
        COUNTERS.incrementSearch();

        SearchRequest searchRequest = new SearchRequestImpl();

        searchRequest.setBase( new Dn( baseDn ) );
        searchRequest.setFilter( filter );
        searchRequest.setScope( scope );
        searchRequest.setSizeLimit( maxEntries );
        searchRequest.setTypesOnly( attrsOnly );
        searchRequest.addAttributes( attrs );

        return connection.search( searchRequest );
    }


    /**
     * This method will search the directory and return at most one record.  If more than one record is found
     * an ldap exception will be thrown.
     *
     * @param connection is LdapConnection object used for all communication with host.
     * @param baseDn     contains address of distinguished name to begin ldap search
     * @param scope      indicates depth of search starting at basedn.  0 (base dn),
     *                   1 (one level down) or 2 (infinite) are valid values.
     * @param filter     contains the search criteria
     * @param attrs      is the requested list of attritubutes to return from directory search.
     * @param attrsOnly  if true pull back attribute names only.
     * @return entry   containing target ldap node.
     * @throws LdapException   thrown in the event of error in ldap client or server code.
     * @throws CursorException If we weren't able to fetch an element from the search result
     */
    protected Entry searchNode( LdapConnection connection, String baseDn, SearchScope scope, String filter,
        String[] attrs, boolean attrsOnly ) throws LdapException, CursorException
    {
        SearchRequest searchRequest = new SearchRequestImpl();

        searchRequest.setBase( new Dn( baseDn ) );
        searchRequest.setFilter( filter );
        searchRequest.setScope( scope );
        searchRequest.setTypesOnly( attrsOnly );
        searchRequest.addAttributes( attrs );

        SearchCursor result = connection.search( searchRequest );

        Entry entry = result.getEntry();

        if ( result.next() )
        {
            throw new LdapException( "searchNode failed to return unique record for LDAP search of base DN [" +
                baseDn + "] filter [" + filter + "]" );
        }

        return entry;
    }


    /**
     * This search method uses OpenLDAP Proxy Authorization Control to assert arbitrary user identity onto connection.
     *
     * @param connection is LdapConnection object used for all communication with host.
     * @param baseDn     contains address of distinguished name to begin ldap search
     * @param scope      indicates depth of search starting at basedn.  0 (base dn),
     *                   1 (one level down) or 2 (infinite) are valid values.
     * @param filter     contains the search criteria
     * @param attrs      is the requested list of attritubutes to return from directory search.
     * @param attrsOnly  if true pull back attribute names only.
     * @param userDn     string value represents the identity of user on who's behalf the request was initiated.  The
     *                   value will be stored in openldap auditsearch record AuthZID's attribute.
     * @return entry   containing target ldap node.
     * @throws LdapException   thrown in the event of error in ldap client or server code.
     * @throws CursorException If we weren't able to fetch an element from the search result
     */
    protected Entry searchNode( LdapConnection connection, String baseDn, SearchScope scope, String filter,
        String[] attrs, boolean attrsOnly, String userDn ) throws LdapException, CursorException
    {
        COUNTERS.incrementSearch();

        SearchRequest searchRequest = new SearchRequestImpl();

        searchRequest.setBase( new Dn( baseDn ) );
        searchRequest.setFilter( filter );
        searchRequest.setScope( scope );
        searchRequest.setTypesOnly( attrsOnly );
        searchRequest.addAttributes( attrs );

        SearchCursor result = connection.search( searchRequest );

        Entry entry = result.getEntry();

        if ( result.next() )
        {
            throw new LdapException( "searchNode failed to return unique record for LDAP search of base DN [" +
                baseDn + "] filter [" + filter + "]" );
        }

        return entry;
    }


    /**
     * This method uses the compare ldap func to assert audit record into the directory server's configured audit
     * logger.
     *
     * This is for one reason - to force the ldap server to maintain an audit trail on checkAccess api.
     *
     * Use proxy authz control (RFC4370) to assert the caller's id onto the record.
     *
     * @param connection is LdapConnection object used for all communication with host.
     * @param dn         contains address of distinguished name to begin ldap search
     * @param userDn     dn for user node
     * @param attribute  attribute used for compare
     * @return true if compare operation succeeds
     * @throws LdapException                thrown in the event of error in ldap client or server code.
     * @throws UnsupportedEncodingException in the event the server cannot perform the operation.
     */
    protected boolean compareNode( LdapConnection connection, String dn, String userDn,
        Attribute attribute ) throws LdapException, UnsupportedEncodingException
    {
        COUNTERS.incrementCompare();

        CompareRequest compareRequest = new CompareRequestImpl();
        compareRequest.setName( new Dn( dn ) );
        compareRequest.setAttributeId( attribute.getId() );
        compareRequest.setAssertionValue( attribute.getString() );

        // Assert the end user's dn onto the reqest using proxy authZ control so openldap can log who the user was (for authZ audit trail)
        ProxiedAuthz proxiedAuthzControl = new ProxiedAuthzImpl();
        proxiedAuthzControl.setAuthzId( "dn: " + userDn );
        compareRequest.addControl( proxiedAuthzControl );
        CompareResponse response = connection.compare( compareRequest );
        return response.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS;
    }


    /**
     * Method wraps ldap client to return multivalued attribute by name within a given entry and returns
     * as a list of strings.
     *
     * @param entry         contains the target ldap entry.
     * @param attributeName name of ldap attribute to retrieve.
     * @return List of type string containing attribute values.
     */
    protected List<String> getAttributes( Entry entry, String attributeName )
    {
        List<String> attrValues = new ArrayList<>();
        if ( entry != null )
        {
            Attribute attr = entry.get( attributeName );
            if ( attr != null )
            {
                for ( Value value : attr )
                {
                    attrValues.add( value.getString() );
                }
            }
            else
            {
                return null;
            }
        }

        return attrValues;
    }


    /**
     * Return the image stored on the entry.
     *
     * @param entry contains the image target.
     * @param attributeName to be retrieved.
     * @return byte array containing image.
     * @throws LdapInvalidAttributeValueException contains the system error.
     */
    protected byte[] getPhoto( Entry entry, String attributeName ) throws LdapInvalidAttributeValueException
    {
        byte[] photo = null;
        Attribute attr = entry.get( attributeName );

        if ( attr != null )
        {
            photo = attr.getBytes();
        }

        return photo;
    }


    /**
     * Method wraps ldap client to return multivalued attribute by name within a given entry and returns
     * as a set of strings.
     *
     * @param entry         contains the target ldap entry.
     * @param attributeName name of ldap attribute to retrieve.
     * @return List of type string containing attribute values.
     */
    protected Set<String> getAttributeSet( Entry entry, String attributeName )
    {
        // create Set with case insensitive comparator:
        Set<String> attrValues = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );

        if ( entry != null && entry.containsAttribute( attributeName ) )
        {
            for ( Value value : entry.get( attributeName ) )
            {
                attrValues.add( value.getString() );
            }
        }

        return attrValues;
    }


    /**
     * Method wraps ldap client to return attribute value by name within a given entry and returns as a string.
     *
     * @param entry         contains the target ldap entry.
     * @param attributeName name of ldap attribute to retrieve.
     * @return value contained in a string variable.
     * @throws LdapInvalidAttributeValueException When we weren't able to get the attribute from the entry
     */
    protected String getAttribute( Entry entry, String attributeName ) throws LdapInvalidAttributeValueException
    {
        if ( entry != null )
        {
            Attribute attr = entry.get( attributeName );

            if ( attr != null )
            {
                return attr.getString();
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }


    /**
     * Method will retrieve the relative distinguished name from a distinguished name variable.
     *
     * @param dn contains ldap distinguished name.
     * @return rDn as string.
     */
    protected String getRdn( String dn )
    {
        try
        {
            return new Dn( dn ).getRdn().getName();
        }
        catch ( LdapInvalidDnException lide )
        {
            return null;
        }
    }


    /**
     * Create multi-occurring ldap attribute given array of strings and attribute name.
     *
     * @param name   contains attribute name to create.
     * @param values array of string that contains attribute values.
     * @return Attribute containing multivalued attribute set.
     * @throws LdapException in the event of ldap client error.
     */
    protected Attribute createAttributes( String name, String values[] ) throws LdapException
    {
        return new DefaultAttribute( name, values );
    }


    /**
     * Convert constraint from raw ldap format to application entity.
     *
     * @param le         ldap entry containing constraint.
     * @param ftDateTime reference to {@link org.apache.directory.fortress.core.model.Constraint} containing formatted data.
     * @throws LdapInvalidAttributeValueException
     *
     * @throws LdapInvalidAttributeValueException when we weren't able to retrieve the attribute from the entry
     */
    protected void unloadTemporal( Entry le, Constraint ftDateTime ) throws LdapInvalidAttributeValueException
    {
        String szRawData = getAttribute( le, GlobalIds.CONSTRAINT );

        if ( szRawData != null && szRawData.length() > 0 )
        {
            ConstraintUtil.setConstraint( szRawData, ftDateTime );
        }
    }


    /**
     * Given an ldap attribute name and a list of attribute values, construct an ldap attribute set to be added to directory.
     *
     * @param list     list of type string containing attribute values to load into attribute set.
     * @param entry    contains ldap attribute set targeted for adding.
     * @param attrName name of ldap attribute being added.
     * @throws LdapException If we weren't able to add the attributes into the entry
     */
    protected void loadAttrs( List<String> list, Entry entry, String attrName ) throws LdapException
    {
        if ( list != null && list.size() > 0 )
        {
            entry.add( attrName, list.toArray( new String[]
                {} ) );
        }
    }


    /**
     * Given an ldap attribute name and a list of attribute values, construct an ldap modification set to be updated
     * in directory.
     *
     * @param list     list of type string containing attribute values to load into modification set.
     * @param mods     contains ldap modification set targeted for updating.
     * @param attrName name of ldap attribute being modified.
     */
    protected void loadAttrs( List<String> list, List<Modification> mods, String attrName )
    {
        if ( ( list != null ) && ( list.size() > 0 ) )
        {
            mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attrName,
                list.toArray( new String[]
                    {} ) ) );
        }
    }


    /**
     * Given a collection of {@link org.apache.directory.fortress.core.model.Relationship}s, convert to raw data name-value format and
     * load into ldap modification set in preparation for ldap modify.
     *
     * @param list     contains List of type {@link org.apache.directory.fortress.core.model.Relationship} targeted for updating in ldap.
     * @param mods     ldap modification set containing parent-child relationships in raw ldap format.
     * @param attrName contains the name of the ldap attribute to be updated.
     * @param op       specifies type of mod: {@link org.apache.directory.fortress.core.model.Hier.Op#ADD},
     * {@link org.apache.directory.fortress.core.model.Hier.Op#MOD}, {@link org.apache.directory.fortress.core.model.Hier.Op#REM}.
     */
    protected void loadRelationshipAttrs( List<Relationship> list, List<Modification> mods, String attrName,
        Hier.Op op )
    {
        if ( list != null )
        {
            Attribute attr;

            for ( Relationship rel : list )
            {
                // This LDAP attr is stored as a name-value pair separated by a ':'.
                attr = new DefaultAttribute( attrName, rel.getChild() + GlobalIds.PROP_SEP + rel.getParent() );

                switch ( op )
                {
                    case ADD:
                        mods.add( new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, attr ) );
                        break;

                    case MOD:
                        mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attr ) );
                        break;

                    case REM:
                        mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, attr ) );
                        break;
                }
            }
        }
    }


    /**
     * Given an ldap attribute name and a set of attribute values, construct an ldap modification set to be updated
     * in directory.
     *
     * @param values   set of type string containing attribute values to load into modification set.
     * @param mods     contains ldap modification set targeted for updating.
     * @param attrName name of ldap attribute being updated.
     */
    protected void loadAttrs( Set<String> values, List<Modification> mods, String attrName )
    {
        if ( ( values != null ) && ( values.size() > 0 ) )
        {
            mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attrName,
                values.toArray( new String[]
                    {} ) ) );
        }
    }


    /**
     * Given an ldap attribute name and a set of attribute values, construct an ldap attribute set to be added to
     * directory.
     *
     * @param values   set of type string containing attribute values to load into attribute set.
     * @param entry    contains ldap entry to pull attrs from.
     * @param attrName name of ldap attribute being added.
     * @throws LdapException If we weren't able to add the values into the entry
     */
    protected void loadAttrs( Set<String> values, Entry entry, String attrName ) throws LdapException
    {
        if ( ( values != null ) && ( values.size() > 0 ) )
        {
            entry.add( attrName, values.toArray( new String[]
                {} ) );
        }
    }


    /**
     * Given a collection of {@link java.util.Properties}, convert to raw data name-value format and load into ldap
     * modification set in preparation for ldap modify.
     *
     * @param props    contains {@link java.util.Properties} targeted for updating in ldap.
     * @param mods     ldap modification set containing name-value pairs in raw ldap format.
     * @param attrName contains the name of the ldap attribute to be updated.
     * @param replace  boolean variable, if set to true use {@link ModificationOperation#REPLACE_ATTRIBUTE} else {@link
     * ModificationOperation#ADD_ATTRIBUTE}.
     */
    protected void loadProperties( Properties props, List<Modification> mods, String attrName, boolean replace )
    {
        loadProperties( props, mods, attrName, replace, GlobalIds.PROP_SEP );
    }


    /**
     * Given a collection of {@link java.util.Properties}, convert to raw data name-value format and load into ldap
     * modification set in preparation for ldap modify.
     *
     * @param props    contains {@link java.util.Properties} targeted for updating in ldap.
     * @param mods     ldap modification set containing name-value pairs in raw ldap format.
     * @param attrName contains the name of the ldap attribute to be updated.
     * @param replace  boolean variable, if set to true use {@link ModificationOperation#REPLACE_ATTRIBUTE} else {@link
     * ModificationOperation#ADD_ATTRIBUTE}.
     * @param separator contains the char value used to separate name and value in ldap raw format.
     */
    protected void loadProperties( Properties props, List<Modification> mods, String attrName, boolean replace,
        char separator )
    {
        if ( props != null && props.size() > 0 )
        {
            if ( replace )
            {
                mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, attrName ) );
            }

            for ( Enumeration<?> e = props.propertyNames(); e.hasMoreElements(); )
            {
                String key = ( String ) e.nextElement();
                String val = props.getProperty( key );
                // This LDAP attr is stored as a name-value pair separated by a ':'.
                mods.add( new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, attrName,
                    key + separator + val ) );
            }
        }
    }


    /**
     * Given a collection of {@link java.util.Properties}, convert to raw data name-value format and load into ldap
     * modification set in preparation for ldap modify.
     *
     * @param props    contains {@link java.util.Properties} targeted for removal from ldap.
     * @param mods     ldap modification set containing name-value pairs in raw ldap format to be removed.
     * @param attrName contains the name of the ldap attribute to be removed.
     */
    protected void removeProperties( Properties props, List<Modification> mods, String attrName )
    {
        if ( props != null && props.size() > 0 )
        {
            for ( Enumeration<?> e = props.propertyNames(); e.hasMoreElements(); )
            {
                String key = ( String ) e.nextElement();
                String val = props.getProperty( key );

                // This LDAP attr is stored as a name-value pair separated by a ':'.
                mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, attrName,
                    key + GlobalIds.PROP_SEP + val ) );
            }
        }
    }


    /**
     * Given a collection of {@link java.util.Properties}, convert to raw data name-value format and load into ldap
     * modification set in preparation for ldap add.
     *
     * @param props    contains {@link java.util.Properties} targeted for adding to ldap.
     * @param entry    contains ldap entry to pull attrs from.
     * @param attrName contains the name of the ldap attribute to be added.
     * @throws LdapException If we weren't able to add the properies into the entry
     */
    protected void loadProperties( Properties props, Entry entry, String attrName ) throws LdapException
    {
        if ( ( props != null ) && ( props.size() > 0 ) )
        {
            Attribute attr = new DefaultAttribute( attrName );

            for ( Enumeration<?> e = props.propertyNames(); e.hasMoreElements(); )
            {
                // This LDAP attr is stored as a name-value pair separated by a ':'.
                String key = ( String ) e.nextElement();
                String val = props.getProperty( key );
                String prop = key + GlobalIds.PROP_SEP + val;

                attr.add( prop );
            }

            if ( attr.size() != 0 )
            {
                entry.add( attr );
            }
        }
    }


    /**
     * Given a collection of {@link java.util.Properties}, convert to raw data name-value format and load into ldap modification set in preparation for ldap add.
     *
     * @param props    contains {@link java.util.Properties} targeted for adding to ldap.
     * @param entry    contains ldap entry to push attrs into.
     * @param attrName contains the name of the ldap attribute to be added.
     * @param separator contains the char value used to separate name and value in ldap raw format.
     * @throws LdapException If we weren't able to add the properies into the entry
     */
    protected void loadProperties( Properties props, Entry entry, String attrName, char separator )
        throws LdapException
    {
        if ( ( props != null ) && ( props.size() > 0 ) )
        {
            Attribute attr = null;

            for ( Enumeration<?> e = props.propertyNames(); e.hasMoreElements(); )
            {
                // This LDAP attr is stored as a name-value pair separated by a ':'.
                String key = ( String ) e.nextElement();
                String val = props.getProperty( key );
                String prop = key + separator + val;

                if ( attr == null )
                {
                    attr = new DefaultAttribute( attrName );
                }
                attr.add( prop );
            }

            if ( attr != null )
            {
                entry.add( attr );
            }
        }
    }


    /**
     * Encode some text so that it can be used in a LDAP filter.
     *
     * @param value The value to encode
     * @param validLen The maximum accepted length of the value. 
     * @return String containing encoded data.
     * @throws LdapException If the value is longer than the maximum value
     */
    protected String encodeSafeText( String value, int validLen ) throws LdapException
    {
        if ( StringUtils.isNotEmpty( value ) )
        {
            int length = value.length();

            if ( length > validLen )
            {
                String error = "encodeSafeText value [" + value + "] invalid length [" + length + "]";
                throw new LdapException( error );
            }

            if ( LdapUtil.getInstance().isLdapfilterSizeFound() )
            {
                value = escapeLDAPSearchFilter( value );
            }
        }

        return value;
    }


    /**
     * Get Password Policy Response Control from LDAP client.
     *
     * @param resp contains reference to LDAP pw policy response.
     * @return PasswordPolicyResponse control.
     */
    protected PasswordPolicyResponse getPwdRespCtrl(BindResponse resp )
    {
        return ( PasswordPolicyResponse ) resp.getControls().get( PasswordPolicyRequest.OID );
    }


    /**
     * Calls the PoolMgr to perform an LDAP bind for a user/password combination.  This function is valid
     * if and only if the user entity is a member of the USERS data set.
     *
     * @param connection connection to ldap server.
     * @param szUserDn   contains the LDAP dn to the user entry in String format.
     * @param password   contains the password in clear text.
     * @return bindResponse contains the result of the operation.
     * @throws LdapException in the event of LDAP error.
     */
    protected BindResponse bind( LdapConnection connection, String szUserDn, String password ) throws LdapException
    {
        COUNTERS.incrementBind();
        Dn userDn = new Dn( szUserDn );
        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( password );
        bindReq.addControl( PP_REQ_CTRL );
        return connection.bind( bindReq );
    }


    /**
     * Calls the PoolMgr to close the Admin LDAP connection.
     *
     * @param connection handle to ldap connection object.
     */
    public void closeAdminConnection( LdapConnection connection )
    {
        LdapConnectionProvider.getInstance().closeAdminConnection(connection);
    }


    /**
     * Calls the PoolMgr to close the Log LDAP connection.
     *
     * @param connection handle to ldap connection object.
     */
    protected void closeLogConnection( LdapConnection connection )
    {
        LdapConnectionProvider.getInstance().closeLogConnection(connection);
    }


    /**
     * Calls the PoolMgr to close the User LDAP connection.
     *
     * @param connection handle to ldap connection object.
     */
    protected void closeUserConnection( LdapConnection connection )
    {
        LdapConnectionProvider.getInstance().closeUserConnection(connection);
    }


    /**
     * Calls the PoolMgr to get an Admin connection to the LDAP server.
     *
     * @return ldap connection.
     * @throws LdapException If we had an issue getting an LDAP connection
     */
    public LdapConnection getAdminConnection() throws LdapException
    {
        return LdapConnectionProvider.getInstance().getAdminConnection();
    }


    /**
     * Calls the PoolMgr to get an Log connection to the LDAP server.
     *
     * @return ldap connection.
     * @throws LdapException If we had an issue getting an LDAP connection
     */
    protected LdapConnection getLogConnection() throws LdapException
    {
        return LdapConnectionProvider.getInstance().getLogConnection();
    }


    /**
     * Calls the PoolMgr to get an User connection to the LDAP server.
     *
     * @return ldap connection.
     * @throws LdapException If we had an issue getting an LDAP connection
     */
    protected LdapConnection getUserConnection() throws LdapException
    {
        return LdapConnectionProvider.getInstance().getUserConnection();
    }


    /**
     * Return to call reference to dao counter object with running totals for ldap operations add, mod, delete, search, etc.
     *
     * @return {@link LdapCounters} contains long values of atomic ldap operations for current running process.
     */
    public static LdapCounters getLdapCounters()
    {
        return COUNTERS;
    }




    /**
     * Perform encoding on supplied input string for certain unsafe ascii characters.  These chars may be unsafe
     * because ldap reserves some characters as operands.  Safe encoding safeguards from malicious scripting input errors 
     * that are possible if data filtering did not get performed before being passed into dao layer.
     *
     * @param filter contains the data to filter.
     * @return possibly modified input string for matched characters.
     */
    protected String escapeLDAPSearchFilter( String filter )
    {
        StringBuilder sb = new StringBuilder();
        int filterLen = filter.length();

        for ( int i = 0; i < filterLen; i++ )
        {
            boolean found = false;
            char curChar = filter.charAt( i );
            int j = 0;

            for ( ; j < GlobalIds.LDAP_FILTER_SIZE; j++ )
            {
                if ( LdapUtil.getInstance().getLdapMetaChars()[j] > curChar )
                {
                    break;
                }
                else if ( curChar == LdapUtil.getInstance().getLdapMetaChars()[j] )
                {
                    sb.append( "\\" );
                    sb.append( LdapUtil.getInstance().getLdapReplVals()[j] );
                    found = true;
                    break;
                }
            }

            if ( !found )
            {
                sb.append( curChar );
            }
        }

        return sb.toString();
    }

    /**
     * Closes all the ldap connection pools.
     */
    public static void closeAllConnectionPools(){
        LdapConnectionProvider.closeAllConnectionPools();
    }

}
