/*
 *   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.impl;


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.DefaultEntry;
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.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.fortress.core.CfgException;
import org.apache.directory.fortress.core.CreateException;
import org.apache.directory.fortress.core.FinderException;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.GlobalIds;
import org.apache.directory.fortress.core.RemoveException;
import org.apache.directory.fortress.core.UpdateException;
import org.apache.directory.fortress.core.ldap.LdapDataProvider;
import org.apache.directory.fortress.core.model.*;
import org.apache.directory.fortress.core.util.Config;
import org.apache.directory.fortress.core.util.PropUpdater;
import org.apache.directory.fortress.core.util.PropUtil;
import org.apache.directory.ldap.client.api.LdapConnection;


/**
 * This class perform data access for Fortress Role entity.
 * <p>
 * The Fortress Role entity is a composite of the following other Fortress structural and aux object classes:
 * <h4>1. ftRls Structural objectclass is used to store the Role information like name and temporal constraint attributes</h4>
 * <ul>
 * <li>  ------------------------------------------
 * <li> <code>objectclass    ( 1.3.6.1.4.1.38088.2.1</code>
 * <li> <code>NAME 'ftRls'</code>
 * <li> <code>DESC 'Fortress Role Object Class'</code>
 * <li> <code>SUP organizationalrole</code>
 * <li> <code>STRUCTURAL</code>
 * <li> <code>MUST ( ftId $ ftRoleName )</code>
 * <li> <code>MAY ( description $ ftCstr ) )</code>
 * <li>  ------------------------------------------
 * </ul>
 * <h4>2. ftProperties AUXILIARY Object Class is used to store client specific name/value pairs on target entity</h4>
 * <code># This aux object class can be used to store custom attributes.</code><br>
 * <code># The properties collections consist of name/value pairs and are not constrainted by Fortress.</code><br>
 * <ul>
 * <li>  ------------------------------------------
 * <li> <code>objectclass ( 1.3.6.1.4.1.38088.3.2</code>
 * <li> <code>NAME 'ftProperties'</code>
 * <li> <code>DESC 'Fortress Properties AUX Object Class'</code>
 * <li> <code>AUXILIARY</code>
 * <li> <code>MAY ( ftProps ) ) </code>
 * <li>  ------------------------------------------
 * </ul>
 * <h4>3. ftMods AUXILIARY Object Class is used to store Fortress audit variables on target entity</h4>
 * <ul>
 * <li> <code>objectclass ( 1.3.6.1.4.1.38088.3.4</code>
 * <li> <code>NAME 'ftMods'</code>
 * <li> <code>DESC 'Fortress Modifiers AUX Object Class'</code>
 * <li> <code>AUXILIARY</code>
 * <li> <code>MAY (</code>
 * <li> <code>ftModifier $</code>
 * <li> <code>ftModCode $</code>
 * <li> <code>ftModId ) )</code>
 * <li>  ------------------------------------------
 * </ul>
 * <p>
 * This class is thread safe.
 *
 * @author Kevin McKinney
 */
final class RoleDAO extends LdapDataProvider implements PropertyProvider<Role>, PropUpdater
{
    /*
      *  *************************************************************************
      *  **  ROLE STATICS contain object and attribute definitions for LDAP operations.
      *  ************************************************************************
      */
    private static final String ROLE_NM = "ftRoleName";

    private static final String[] ROLE_NM_ATR =
        {
            ROLE_NM
    };


    // rfc2307 decls:
    private static final String POSIX_GROUP = "posixGroup";
    static final boolean IS_RFC2307 = Config.getInstance().getProperty( GlobalIds.RFC2307_PROP ) != null && Config.getInstance().getProperty( GlobalIds.RFC2307_PROP ).equalsIgnoreCase( "true" ) ? true : false;

    private static final String[] ROLE_ATRS =
    {
            GlobalIds.FT_IID,
            ROLE_NM,
            SchemaConstants.DESCRIPTION_AT,
            GlobalIds.CONSTRAINT,
            SchemaConstants.ROLE_OCCUPANT_AT,
            GlobalIds.PARENT_NODES,
            GlobalIds.PROPS,
            IS_RFC2307 ? GlobalIds.GID_NUMBER : null
    };

    private static final String[] ROLE_CONSTRAINTS =
    {
            ROLE_NM,
            GlobalIds.CONSTRAINT,
            GlobalIds.PARENT_NODES
    };

    /**
     * Defines the object class structure used within Fortress Role processing.
     */
    private static String[] ROLE_OBJ_CLASS = IS_RFC2307 ? new String[]
        {
            SchemaConstants.TOP_OC,
            GlobalIds.ROLE_OBJECT_CLASS_NM,
            GlobalIds.PROPS_AUX_OBJECT_CLASS_NAME,
            GlobalIds.FT_MODIFIER_AUX_OBJECT_CLASS_NAME,
            POSIX_GROUP
        }
        : new String[]
        {
            SchemaConstants.TOP_OC,
            GlobalIds.ROLE_OBJECT_CLASS_NM,
            GlobalIds.PROPS_AUX_OBJECT_CLASS_NAME,
            GlobalIds.FT_MODIFIER_AUX_OBJECT_CLASS_NAME
        };

    /**
     * Method on PropUdater interface used to increment UID and GID prop values.
     * @param value contains a String that will be converted to an Integer before incremeting.
     * @return String value contains the new sequence value.
     */
    public String newValue(String value)
    {
        Integer id = new Integer( value );
        Integer newId = id + 1;
        return newId.toString();
    }

    /**
     * @param entity
     * @return
     * @throws CreateException
     */
    Role create( Role entity ) throws CreateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );

        try
        {
            Entry entry = new DefaultEntry( dn );
            entry.add( SchemaConstants.OBJECT_CLASS_AT, ROLE_OBJ_CLASS );
            entity.setId();
            entry.add( GlobalIds.FT_IID, entity.getId() );
            entry.add( ROLE_NM, entity.getName() );
            // description field is optional on this object class:
            if ( StringUtils.isNotEmpty( entity.getDescription() ) )
            {
                entry.add( SchemaConstants.DESCRIPTION_AT, entity.getDescription() );
            }

            // CN attribute is required for this object class:
            entry.add( SchemaConstants.CN_AT, entity.getName() );
            entry.add( GlobalIds.CONSTRAINT, ConstraintUtil.setConstraint( entity ) );

            // These multi-valued attributes are optional.  The utility function will return quietly if items are not loaded into collection:
            loadAttrs( entity.getParents(), entry, GlobalIds.PARENT_NODES );

            if ( IS_RFC2307 )
            {
                // Supporting RFC2307 posixGroups attributes on fortress roles.
                loadGidNumber( entity );
                entry.add( GlobalIds.GID_NUMBER, entity.getGidNumber() );
            }

            ld = getAdminConnection();
            add( ld, entry, entity );
        }
        catch ( LdapException e )
        {
            String error = "create role [" + entity.getName() + "] caught LdapException=" + e;
            throw new CreateException( GlobalErrIds.ROLE_ADD_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return entity;
    }

    /**
     * @param entity
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     *
     */
    Role update( Role entity ) throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );

        try
        {
            List<Modification> mods = new ArrayList<Modification>();

            if ( StringUtils.isNotEmpty( entity.getDescription() ) )
            {
                mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                    SchemaConstants.DESCRIPTION_AT, entity.getDescription() ) );
            }

            if ( entity.isTemporalSet() )
            {
                String szRawData = ConstraintUtil.setConstraint( entity );

                if ( StringUtils.isNotEmpty( szRawData ) )
                {
                    mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                        GlobalIds.CONSTRAINT, szRawData ) );
                }
            }

            loadAttrs( entity.getParents(), mods, GlobalIds.PARENT_NODES );

            if ( IS_RFC2307 && StringUtils.isNotEmpty( entity.getGidNumber() ) )
            {
                mods.add( new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
                        GlobalIds.GID_NUMBER, entity.getGidNumber() ) );
            }

            if ( mods.size() > 0 )
            {
                ld = getAdminConnection();
                modify( ld, dn, mods, entity );
            }
        }
        catch ( LdapException e )
        {
            String error = "update name [" + entity.getName() + "] caught LdapException=" + e;
            throw new UpdateException( GlobalErrIds.ROLE_UPDATE_FAILED, error, e );
        }
        catch ( Exception e )
        {
            String error = "update name [" + entity.getName() + "] caught LdapException=" + e.getMessage();
            throw new UpdateException( GlobalErrIds.ROLE_UPDATE_FAILED, error, e );
        }
        finally
        {
            try
            {
                closeAdminConnection( ld );
            }
            catch ( Exception e )
            {
                String error = "update name [" + entity.getName() + "] caught LdapException=" + e;
                throw new UpdateException( GlobalErrIds.ROLE_UPDATE_FAILED, error, e );
            }
        }

        return entity;
    }


    /**
     *
     * @param entity
     * @throws UpdateException
     */
    void deleteParent( Role entity ) throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );
        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
                GlobalIds.PARENT_NODES ) );
            ld = getAdminConnection();
            modify( ld, dn, mods, entity );
        }
        catch ( LdapException e )
        {
            String error = "deleteParent name [" + entity.getName() + "] caught LdapException=" + e;
            throw new UpdateException( GlobalErrIds.ROLE_REMOVE_PARENT_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
    }


    /**
     * @param entity
     * @param userDn
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     *
     */
    Role assign( Role entity, String userDn ) throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );

        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, SchemaConstants.ROLE_OCCUPANT_AT,
                userDn ) );
            ld = getAdminConnection();
            modify( ld, dn, mods, entity );
        }
        catch ( LdapException e )
        {
            String error = "assign role name [" + entity.getName() + "] user dn [" + userDn + "] caught LdapException="
                + e;
            throw new UpdateException( GlobalErrIds.ROLE_USER_ASSIGN_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return entity;
    }


    /**
     * @param entity
     * @param userDn
     * @return
     * @throws org.apache.directory.fortress.core.UpdateException
     *
     */
    Role deassign( Role entity, String userDn ) throws UpdateException
    {
        LdapConnection ld = null;
        String dn = getDn( entity.getName(), entity.getContextId() );
        try
        {
            List<Modification> mods = new ArrayList<Modification>();
            mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
                SchemaConstants.ROLE_OCCUPANT_AT, userDn ) );
            ld = getAdminConnection();
            modify( ld, dn, mods, entity );
        }
        catch ( LdapException e )
        {
            String error = "deassign role name [" + entity.getName() + "] user dn [" + userDn
                + "] caught LdapException=" + e;
            throw new UpdateException( GlobalErrIds.ROLE_USER_DEASSIGN_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return entity;
    }


    /**
     * @param role
     * @throws RemoveException
     */
    void remove( Role role )
        throws RemoveException
    {
        LdapConnection ld = null;
        String dn = getDn( role.getName(), role.getContextId() );

        try
        {
            ld = getAdminConnection();
            delete( ld, dn, role );
        }
        catch ( LdapException e )
        {
            String error = "remove role name=" + role.getName() + " LdapException=" + e;
            throw new RemoveException( GlobalErrIds.ROLE_DELETE_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }
    }


    /**
     * @param role
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    Role getRole( Role role )
        throws FinderException
    {
        Role entity = null;
        LdapConnection ld = null;
        String dn = getDn( role.getName(), role.getContextId() );

        try
        {
            ld = getAdminConnection();
            Entry findEntry = read( ld, dn, ROLE_ATRS );
            if ( findEntry != null )
            {
                entity = unloadLdapEntry( findEntry, 0, role.getContextId() );
            }
            if ( entity == null )
            {
                String warning = "getRole no entry found dn [" + dn + "]";
                throw new FinderException( GlobalErrIds.ROLE_NOT_FOUND, warning );
            }
        }
        catch ( LdapNoSuchObjectException e )
        {
            String warning = "getRole Obj COULD NOT FIND ENTRY for dn [" + dn + "]";
            throw new FinderException( GlobalErrIds.ROLE_NOT_FOUND, warning );
        }
        catch ( LdapException e )
        {
            String error = "getRole dn [" + dn + "] LEXCD=" + e;
            throw new FinderException( GlobalErrIds.ROLE_READ_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return entity;
    }


    Role getConstraints( Role role )
        throws FinderException
    {
        Role entity = null;
        LdapConnection ld = null;
        String dn = getDn( role.getName(), role.getContextId() );

        try
        {
            ld = getAdminConnection();
            Entry findEntry = read( ld, dn, ROLE_CONSTRAINTS );
            if ( findEntry != null )
            {
                entity = unloadLdapEntry( findEntry, 0, role.getContextId() );
            }
            if ( entity == null )
            {
                String warning = "getConstraints no entry found dn [" + dn + "]";
                throw new FinderException( GlobalErrIds.ROLE_NOT_FOUND, warning );
            }
        }
        catch ( LdapNoSuchObjectException e )
        {
            String warning = "getConstraints Obj COULD NOT FIND ENTRY for dn [" + dn + "]";
            throw new FinderException( GlobalErrIds.ROLE_NOT_FOUND, warning );
        }
        catch ( LdapException e )
        {
            String error = "getConstraints dn [" + dn + "] LEXCD=" + e;
            throw new FinderException( GlobalErrIds.ROLE_READ_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return entity;
    }


    /**
     * @param role
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<Role> findRoles( Role role )
        throws FinderException
    {
        List<Role> roleList = new ArrayList<>();
        LdapConnection ld = null;
        String roleRoot = getRootDn( role.getContextId(), GlobalIds.ROLE_ROOT );
        String filter = null;

        try
        {
            String searchVal = encodeSafeText( role.getName(), GlobalIds.ROLE_LEN );
            filter = GlobalIds.FILTER_PREFIX + GlobalIds.ROLE_OBJECT_CLASS_NM + ")("
                + ROLE_NM + "=" + searchVal + "*))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, roleRoot,
                SearchScope.ONELEVEL, filter, ROLE_ATRS, false, Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                long sequence = 0;
                while ( searchResults.next() )
                {
                    roleList.add( unloadLdapEntry( searchResults.getEntry(), sequence++, role.getContextId() ) );
                }
            }
            catch ( IOException e )
            {
                String error = "findRoles filter [" + filter + "] caught IOException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
            catch ( CursorException e )
            {
                String error = "findRoles filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "findRoles filter [" + filter + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return roleList;
    }


    /**
     * Pull back all roles that are assigned to a particular group.
     * @param group
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<Role> groupRoles ( Group group ) throws FinderException
    {
        List<Role> roleList = new ArrayList<>();
        LdapConnection ld = null;
        String roleRoot = getRootDn( group.getContextId(), GlobalIds.ROLE_ROOT );
        StringBuilder filterbuf = new StringBuilder();

        try
        {
            // loop for each group member....
            // add role name to search filter
            //
            List<String> members = group.getMembers();
            if ( CollectionUtils.isNotEmpty( members ) )
            {
                filterbuf.append( GlobalIds.FILTER_PREFIX );
                filterbuf.append( GlobalIds.ROLE_OBJECT_CLASS_NM );
                filterbuf.append( ")(" );
                filterbuf.append( "|" );
                for ( String memberdn : members )
                {
                    filterbuf.append( "(" );
                    filterbuf.append( SchemaConstants.ENTRY_DN_AT );
                    filterbuf.append( "=" );
                    filterbuf.append( memberdn );
                    filterbuf.append( ")" );
                }
                filterbuf.append( "))" );

                ld = getAdminConnection();
                try ( SearchCursor searchResults = search( ld, roleRoot,
                    SearchScope.ONELEVEL, filterbuf.toString(), ROLE_ATRS, false, Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
                {
                    long sequence = 0;
                    while ( searchResults.next() )
                    {
                        roleList.add( unloadLdapEntry( searchResults.getEntry(), sequence++, group.getContextId() ) );
                    }
                }
                catch ( IOException e )
                {
                    String error = "groupRoles filter [" + filterbuf.toString() + "] caught IOException=" + e.getMessage();
                    throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
                }
                catch ( CursorException e )
                {
                    String error = "groupRoles filter [" + filterbuf.toString() + "] caught CursorException=" + e.getMessage();
                    throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
                }
            }
            else
            {
                String error = "groupRoles passed empty member list";
                throw new FinderException( GlobalErrIds.GROUP_MEMBER_NULL, error );
            }
        }
        catch ( LdapException e )
        {
            String error = "groupRoles filter [" + filterbuf.toString() + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return roleList;
    }


    /**
     * @param role
     * @param limit
     * @return
     * @throws org.apache.directory.fortress.core.FinderException
     *
     */
    List<String> findRoles( Role role, int limit )
        throws FinderException
    {
        List<String> roleList = new ArrayList<>();
        LdapConnection ld = null;
        String roleRoot = getRootDn( role.getContextId(), GlobalIds.ROLE_ROOT );
        String filter = null;

        try
        {
            String searchVal = encodeSafeText( role.getName(), GlobalIds.ROLE_LEN );
            filter = GlobalIds.FILTER_PREFIX + GlobalIds.ROLE_OBJECT_CLASS_NM + ")("
                + ROLE_NM + "=" + searchVal + "*))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, roleRoot,
                SearchScope.ONELEVEL, filter, ROLE_NM_ATR, false, limit ) )
            {
                while ( searchResults.next() )
                {
                    Entry entry = searchResults.getEntry();
                    roleList.add( getAttribute( entry, ROLE_NM ) );
                }
            }
            catch ( IOException e )
            {
                String error = "findRoles filter [" + filter + "] caught IOException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
            catch ( CursorException e )
            {
                String error = "findRoles filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "findRoles filter [" + filter + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return roleList;
    }


    /**
     *
     * @param userDn
     * @param contextId
     * @return
     * @throws FinderException
     */
    List<String> findAssignedRoles( String userDn, String contextId )
        throws FinderException
    {
        List<String> roleNameList = new ArrayList<>();
        LdapConnection ld = null;
        String roleRoot = getRootDn( contextId, GlobalIds.ROLE_ROOT );

        try
        {
            String filter = GlobalIds.FILTER_PREFIX + GlobalIds.ROLE_OBJECT_CLASS_NM + ")";
            filter += "(" + SchemaConstants.ROLE_OCCUPANT_AT + "=" + userDn + "))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, roleRoot,
                SearchScope.ONELEVEL, filter, ROLE_NM_ATR, false, Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                while ( searchResults.next() )
                {
                    roleNameList.add( getAttribute( searchResults.getEntry(), ROLE_NM ) );
                }
            }
            catch ( IOException e )
            {
                String error = "findAssignedRoles userDn [" + userDn + "] caught IOException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_OCCUPANT_SEARCH_FAILED, error, e );
            }
            catch ( CursorException e )
            {
                String error = "findAssignedRoles userDn [" + userDn + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_OCCUPANT_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "findAssignedRoles userDn [" + userDn + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.ROLE_OCCUPANT_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return roleNameList;
    }


    /**
     *
     * @param contextId
     * @return
     * @throws FinderException
     */
    List<Graphable> getAllDescendants( String contextId )
        throws FinderException
    {
        String[] DESC_ATRS =
            { ROLE_NM, GlobalIds.PARENT_NODES };
        List<Graphable> descendants = new ArrayList<>();
        LdapConnection ld = null;
        String roleRoot = getRootDn( contextId, GlobalIds.ROLE_ROOT );
        String filter = null;

        try
        {
            filter = GlobalIds.FILTER_PREFIX + GlobalIds.ROLE_OBJECT_CLASS_NM + ")("
                + GlobalIds.PARENT_NODES + "=*))";
            ld = getAdminConnection();
            try ( SearchCursor searchResults = search( ld, roleRoot,
                SearchScope.ONELEVEL, filter, DESC_ATRS, false, Config.getInstance().getInt(GlobalIds.CONFIG_LDAP_MAX_BATCH_SIZE, GlobalIds.BATCH_SIZE ) ) )
            {
                long sequence = 0;
                while ( searchResults.next() )
                {
                    descendants.add( unloadDescendants( searchResults.getEntry(), sequence++, contextId ) );
                }
            }
            catch ( IOException e )
            {
                String error = "getAllDescendants filter [" + filter + "] caught IOException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
            catch ( CursorException e )
            {
                String error = "getAllDescendants filter [" + filter + "] caught CursorException=" + e.getMessage();
                throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
            }
        }
        catch ( LdapException e )
        {
            String error = "getAllDescendants filter [" + filter + "] caught LdapException=" + e;
            throw new FinderException( GlobalErrIds.ROLE_SEARCH_FAILED, error, e );
        }
        finally
        {
            closeAdminConnection( ld );
        }

        return descendants;
    }


    /**
     * Sets the value of gidNumber on Role entity. Will use what's passed in or auto increment current.
     *
     * @param entity
     * @throws CreateException
     */
    private void loadGidNumber( Role entity ) throws CreateException
    {
        // Generate the value of gidNumber if not passed in by caller:
        if ( StringUtils.isEmpty( entity.getGidNumber() ) )
        {
            List<String> idNumbers = new ArrayList<>();
            idNumbers.add(GlobalIds.GID_NUMBER);
            Configuration configuration;
            try
            {
                configuration = Config.getInstance().getIncrementReplacePosixIds(idNumbers, this);
            }
            catch (CfgException ce)
            {
                String error = "Create role had a problem loading the gidNumber, catching a CfgException:" + ce.getMessage();
                throw new CreateException(GlobalErrIds.USER_ADD_FAILED, error, ce);
            }
            entity.setGidNumber( configuration.getGidNumber() );
        }
    }

    /**
     *
     * @param le
     * @param sequence
     * @param contextId
     * @return
     * @throws LdapInvalidAttributeValueException 
     * @throws LdapException
     */
    private Graphable unloadDescendants( Entry le, long sequence, String contextId )
        throws LdapInvalidAttributeValueException
    {
        Role entity = new ObjectFactory().createRole();
        entity.setSequenceId( sequence );
        entity.setName( getAttribute( le, ROLE_NM ) );
        entity.setParents( getAttributeSet( le, GlobalIds.PARENT_NODES ) );
        return entity;
    }


    /**
     *
     * @param le
     * @param sequence
     * @param contextId
     * @return
     * @throws LdapInvalidAttributeValueException 
     * @throws LdapException
     */
    private Role unloadLdapEntry( Entry le, long sequence, String contextId ) throws LdapInvalidAttributeValueException
    {
        Role entity = new ObjectFactory().createRole();
        entity.setSequenceId( sequence );
        entity.setId( getAttribute( le, GlobalIds.FT_IID ) );
        entity.setName( getAttribute( le, ROLE_NM ) );
        entity.setDescription( getAttribute( le, SchemaConstants.DESCRIPTION_AT ) );
        entity.setChildren( RoleUtil.getInstance().getChildren( entity.getName().toUpperCase(), contextId ) );
        entity.setParents( getAttributeSet( le, GlobalIds.PARENT_NODES ) );        
        unloadTemporal( le, entity );
        entity.setDn( le.getDn().getName() );        
        entity.addProperties( PropUtil.getProperties( getAttributes( le, GlobalIds.PROPS ) ) );
        if ( IS_RFC2307 )
        {
            entity.setGidNumber( getAttribute( le, GlobalIds.GID_NUMBER ) );
        }
        entity.setOccupants( getAttributes( le, SchemaConstants.ROLE_OCCUPANT_AT ) );
        return entity;
    }


    private String getDn( String name, String contextId )
    {
        return SchemaConstants.CN_AT + "=" + name + "," + getRootDn( contextId, GlobalIds.ROLE_ROOT );
    }


    @Override
    public String getDn( Role entity )
    {
        return this.getDn( entity.getName(), entity.getContextId() );
    }


    @Override
    public Role getEntity( Role entity ) throws FinderException
    {
        return this.getRole( entity );
    }
}
