/*
 *   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.directory.server.core.shared;


import java.io.File;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.directory.api.ldap.extras.controls.syncrepl.syncRequest.SyncRequestValue;
import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.EmptyCursor;
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.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidSearchFilterException;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.FilterParser;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.AddRequest;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.message.CompareRequest;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.DeleteRequest;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.ModifyDnRequest;
import org.apache.directory.api.ldap.model.message.ModifyRequest;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.ResultResponse;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.message.UnbindRequest;
import org.apache.directory.api.ldap.model.message.controls.SortKey;
import org.apache.directory.api.ldap.model.message.controls.SortRequest;
import org.apache.directory.api.ldap.model.message.controls.SortResponse;
import org.apache.directory.api.ldap.model.message.controls.SortResponseImpl;
import org.apache.directory.api.ldap.model.message.controls.SortResultCode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.LdapPrincipal;
import org.apache.directory.server.core.api.OperationManager;
import org.apache.directory.server.core.api.changelog.LogChange;
import org.apache.directory.server.core.api.interceptor.context.AbstractOperationContext;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.CompareOperationContext;
import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.api.interceptor.context.OperationContext;
import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
import org.apache.directory.server.core.api.interceptor.context.UnbindOperationContext;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.partition.PartitionTxn;
import org.apache.directory.server.i18n.I18n;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jdbm.recman.BaseRecordManager;


/**
 * The default CoreSession implementation.
 * 
 * TODO - has not been completed yet
 * TODO - need to supply controls and other parameters to setup opContexts
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class DefaultCoreSession implements CoreSession
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( DefaultCoreSession.class );

    /** The DirectoryService we are connected to */
    private final DirectoryService directoryService;

    /** The Principal used to process operations */
    private final LdapPrincipal authenticatedPrincipal;

    /** The anonymous principal, if we have to process operation as anonymous */
    private final LdapPrincipal anonymousPrincipal;

    /** The authorized principal, which will be used when a user has been authorized */
    private LdapPrincipal authorizedPrincipal;

    /** A reference to the ObjectClass AT */
    protected AttributeType objectClassAT;

    /** The associated IoSession */
    private IoSession ioSession;

    /** flag to indicate if the password must be changed */
    private boolean pwdMustChange;

    /** A flag set when the startTransaction extended operation has been received */
    private boolean hasSessionTransaction;
    
    /** The Map containing the transactions associated with each partition */
    private Map<String, PartitionTxn> transactionMap = new HashMap<>();
    
    /** The transaction ID */
    private AtomicLong transactionId = new AtomicLong( 0 );

    /**
     * Creates a new instance of a DefaultCoreSession
     * @param principal The principal to use to process operation for this session
     * @param directoryService The DirectoryService to which we will send requests
     */
    public DefaultCoreSession( LdapPrincipal principal, DirectoryService directoryService )
    {
        this.directoryService = directoryService;
        authenticatedPrincipal = principal;

        if ( principal.getAuthenticationLevel() == AuthenticationLevel.NONE )
        {
            anonymousPrincipal = principal;
        }
        else
        {
            anonymousPrincipal = new LdapPrincipal( directoryService.getSchemaManager() );
        }

        // setup attribute type value
        objectClassAT = directoryService.getSchemaManager().getAttributeType( SchemaConstants.OBJECT_CLASS_AT );
    }


    /**
     * Gets the IoSession from the CoreSession. This is only useful when the server is not embedded.
     * 
     * @return ioSession The IoSession for this CoreSession
     */
    public IoSession getIoSession()
    {
        return ioSession;
    }


    /**
     * Stores the IoSession into the CoreSession. This is only useful when the server is not embedded.
     * 
     * @param ioSession The IoSession for this CoreSession
     */
    public void setIoSession( IoSession ioSession )
    {
        this.ioSession = ioSession;
    }


    /**
     * Set the ignoreRefferal flag for the current operationContext.
     *
     * @param opContext The current operationContext
     * @param ignoreReferral The flag
     */
    private void setReferralHandling( AbstractOperationContext opContext, boolean ignoreReferral )
    {
        if ( ignoreReferral )
        {
            opContext.ignoreReferral();
        }
        else
        {
            opContext.throwReferral();
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( Entry entry ) throws LdapException
    {
        add( entry, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( Entry entry, boolean ignoreReferral ) throws LdapException
    {
        add( entry, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( Entry entry, LogChange log ) throws LdapException
    {
        AddOperationContext addContext = new AddOperationContext( this, entry );

        addContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.add( addContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( Entry entry, boolean ignoreReferral, LogChange log ) throws LdapException
    {
        AddOperationContext addContext = new AddOperationContext( this, entry );

        addContext.setLogChange( log );
        setReferralHandling( addContext, ignoreReferral );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.add( addContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( AddRequest addRequest ) throws LdapException
    {
        add( addRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( AddRequest addRequest, LogChange log ) throws LdapException
    {
        AddOperationContext addContext = new AddOperationContext( this, addRequest );

        addContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        
        try
        {
            operationManager.add( addContext );
        }
        catch ( LdapException e )
        {
            addRequest.getResultResponse().addAllControls( addContext.getResponseControls() );
            throw e;
        }
        
        addRequest.getResultResponse().addAllControls( addContext.getResponseControls() );
    }


    private Value convertToValue( String oid, Object value ) throws LdapException
    {
        Value val;

        AttributeType attributeType = directoryService.getSchemaManager().lookupAttributeTypeRegistry( oid );

        // make sure we add the request controls to operation
        if ( attributeType.getSyntax().isHumanReadable() )
        {
            if ( value instanceof String )
            {
                val = new Value( attributeType, ( String ) value );
            }
            else if ( value instanceof byte[] )
            {
                val = new Value( attributeType, Strings.utf8ToString( ( byte[] ) value ) );
            }
            else
            {
                throw new LdapException( I18n.err( I18n.ERR_309, oid ) );
            }
        }
        else
        {
            if ( value instanceof String )
            {
                val = new Value( attributeType, Strings.getBytesUtf8( ( String ) value ) );
            }
            else if ( value instanceof byte[] )
            {
                val = new Value( attributeType, ( byte[] ) value );
            }
            else
            {
                throw new LdapException( I18n.err( I18n.ERR_309, oid ) );
            }
        }

        return val;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean compare( Dn dn, String oid, Object value ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();

        return operationManager.compare( new CompareOperationContext( this, dn, oid, convertToValue( oid, value ) ) );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean compare( Dn dn, String oid, Object value, boolean ignoreReferral ) throws LdapException
    {
        CompareOperationContext compareContext = new CompareOperationContext( this, dn, oid,
            convertToValue( oid, value ) );

        setReferralHandling( compareContext, ignoreReferral );

        OperationManager operationManager = directoryService.getOperationManager();
        return operationManager.compare( compareContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( Dn dn ) throws LdapException
    {
        delete( dn, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( Dn dn, LogChange log ) throws LdapException
    {
        DeleteOperationContext deleteContext = new DeleteOperationContext( this, dn );

        deleteContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.delete( deleteContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( Dn dn, boolean ignoreReferral ) throws LdapException
    {
        delete( dn, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( Dn dn, boolean ignoreReferral, LogChange log ) throws LdapException
    {
        DeleteOperationContext deleteContext = new DeleteOperationContext( this, dn );

        deleteContext.setLogChange( log );
        setReferralHandling( deleteContext, ignoreReferral );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.delete( deleteContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public LdapPrincipal getAnonymousPrincipal()
    {
        return anonymousPrincipal;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public LdapPrincipal getAuthenticatedPrincipal()
    {
        return authenticatedPrincipal;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public AuthenticationLevel getAuthenticationLevel()
    {
        return getEffectivePrincipal().getAuthenticationLevel();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public SocketAddress getClientAddress()
    {
        if ( ioSession != null )
        {
            return ioSession.getRemoteAddress();
        }
        else
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Set<Control> getControls()
    {
        // TODO Auto-generated method stub
        return null;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public DirectoryService getDirectoryService()
    {
        return directoryService;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public LdapPrincipal getEffectivePrincipal()
    {
        if ( authorizedPrincipal == null )
        {
            return authenticatedPrincipal;
        }

        return authorizedPrincipal;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Set<OperationContext> getOutstandingOperations()
    {
        // TODO Auto-generated method stub
        return null;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public SocketAddress getServiceAddress()
    {
        if ( ioSession != null )
        {
            return ioSession.getServiceAddress();
        }
        else
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isConfidential()
    {
        // TODO Auto-generated method stub
        return false;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isVirtual()
    {
        // TODO Auto-generated method stub
        return true;
    }


    /**
     * TODO - perhaps we should just use a flag that is calculated on creation
     * of this session
     * 
     * @see org.apache.directory.server.core.api.CoreSession#isAdministrator()
     */
    @Override
    public boolean isAdministrator()
    {
        String normName = getEffectivePrincipal().getName();

        return normName.equals( ServerDNConstants.ADMIN_SYSTEM_DN_NORMALIZED );
    }


    /**
     * TODO - this method impl does not check to see if the principal is in
     * the administrators group - it only returns true of the principal is
     * the actual admin user.  need to make it check groups.
     * 
     * TODO - perhaps we should just use a flag that is calculated on creation
     * of this session
     * 
     * @see org.apache.directory.server.core.api.CoreSession#isAnAdministrator()
     */
    @Override
    public boolean isAnAdministrator()
    {
        if ( isAdministrator() )
        {
            return true;
        }

        // TODO fix this so it checks groups
        return false;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor<Entry> list( Dn dn, AliasDerefMode aliasDerefMode,
        String... returningAttributes ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();

        PresenceNode filter = new PresenceNode( objectClassAT );
        SearchOperationContext searchContext = new SearchOperationContext( this, dn, SearchScope.ONELEVEL, filter,
            returningAttributes );
        searchContext.setAliasDerefMode( aliasDerefMode );

        return operationManager.search( searchContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry lookup( Dn dn, String... attrIds ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        LookupOperationContext lookupContext = new LookupOperationContext( this, dn, attrIds );

        return operationManager.lookup( lookupContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry lookup( Dn dn, Control[] controls, String... attrIds ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        LookupOperationContext lookupContext = new LookupOperationContext( this, dn, attrIds );

        if ( controls != null )
        {
            lookupContext.addRequestControls( controls );
        }

        return operationManager.lookup( lookupContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( Dn dn, Modification... mods ) throws LdapException
    {
        modify( dn, Arrays.asList( mods ), LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( Dn dn, List<Modification> mods ) throws LdapException
    {
        modify( dn, mods, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( Dn dn, List<Modification> mods, LogChange log ) throws LdapException
    {
        if ( mods == null )
        {
            return;
        }

        List<Modification> serverModifications = new ArrayList<>( mods.size() );

        for ( Modification mod : mods )
        {
            serverModifications.add( new DefaultModification( directoryService.getSchemaManager(), mod ) );
        }

        ModifyOperationContext modifyContext = new ModifyOperationContext( this, dn, serverModifications );

        modifyContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        
        operationManager.modify( modifyContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( Dn dn, List<Modification> mods, boolean ignoreReferral ) throws LdapException
    {
        modify( dn, mods, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( Dn dn, List<Modification> mods, boolean ignoreReferral, LogChange log ) throws LdapException
    {
        if ( mods == null )
        {
            return;
        }

        List<Modification> serverModifications = new ArrayList<>( mods.size() );

        for ( Modification mod : mods )
        {
            serverModifications.add( new DefaultModification( directoryService.getSchemaManager(), mod ) );
        }

        ModifyOperationContext modifyContext = new ModifyOperationContext( this, dn, serverModifications );

        setReferralHandling( modifyContext, ignoreReferral );
        modifyContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.modify( modifyContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( Dn dn, Dn newParent ) throws LdapException
    {
        move( dn, newParent, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( Dn dn, Dn newParent, LogChange log ) throws LdapException
    {
        MoveOperationContext moveContext = new MoveOperationContext( this, dn, newParent );
        moveContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.move( moveContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( Dn dn, Dn newParent, boolean ignoreReferral ) throws Exception
    {
        move( dn, newParent, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( Dn dn, Dn newParent, boolean ignoreReferral, LogChange log ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        MoveOperationContext moveContext = new MoveOperationContext( this, dn, newParent );

        setReferralHandling( moveContext, ignoreReferral );
        moveContext.setLogChange( log );

        operationManager.move( moveContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( Dn dn, Dn newParent, Rdn newRdn, boolean deleteOldRdn ) throws LdapException
    {
        moveAndRename( dn, newParent, newRdn, deleteOldRdn, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( Dn dn, Dn newSuperiorDn, Rdn newRdn, boolean deleteOldRdn, LogChange log )
        throws LdapException
    {
        MoveAndRenameOperationContext moveAndRenameContext = new MoveAndRenameOperationContext( this, dn,
            newSuperiorDn, newRdn, deleteOldRdn );

        moveAndRenameContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.moveAndRename( moveAndRenameContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( Dn dn, Dn newParent, Rdn newRdn, boolean deleteOldRdn, boolean ignoreReferral )
        throws LdapException
    {
        moveAndRename( dn, newParent, newRdn, deleteOldRdn, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( Dn dn, Dn newParent, Rdn newRdn, boolean deleteOldRdn, boolean ignoreReferral,
        LogChange log ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        MoveAndRenameOperationContext moveAndRenameContext = new MoveAndRenameOperationContext( this, dn, newParent,
            newRdn, deleteOldRdn );

        moveAndRenameContext.setLogChange( log );
        setReferralHandling( moveAndRenameContext, ignoreReferral );

        operationManager.moveAndRename( moveAndRenameContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( Dn dn, Rdn newRdn, boolean deleteOldRdn ) throws LdapException
    {
        rename( dn, newRdn, deleteOldRdn, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( Dn dn, Rdn newRdn, boolean deleteOldRdn, LogChange log ) throws LdapException
    {
        RenameOperationContext renameContext = new RenameOperationContext( this, dn, newRdn, deleteOldRdn );

        renameContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        operationManager.rename( renameContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( Dn dn, Rdn newRdn, boolean deleteOldRdn, boolean ignoreReferral ) throws LdapException
    {
        rename( dn, newRdn, deleteOldRdn, ignoreReferral, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( Dn dn, Rdn newRdn, boolean deleteOldRdn, boolean ignoreReferral, LogChange log )
        throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        RenameOperationContext renameContext = new RenameOperationContext( this, dn, newRdn, deleteOldRdn );

        renameContext.setLogChange( log );
        setReferralHandling( renameContext, ignoreReferral );

        operationManager.rename( renameContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor<Entry> search( Dn dn, String filter ) throws LdapException
    {
        return search( dn, filter, true );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor<Entry> search( Dn dn, String filter, boolean ignoreReferrals ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();
        ExprNode filterNode = null;

        try
        {
            filterNode = FilterParser.parse( directoryService.getSchemaManager(), filter );
        }
        catch ( ParseException pe )
        {
            throw new LdapInvalidSearchFilterException( pe.getMessage() );
        }

        SearchOperationContext searchContext = new SearchOperationContext( this, dn, SearchScope.OBJECT, filterNode,
            ( String ) null );
        searchContext.setAliasDerefMode( AliasDerefMode.DEREF_ALWAYS );
        setReferralHandling( searchContext, ignoreReferrals );

        return operationManager.search( searchContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor<Entry> search( Dn dn, SearchScope scope, ExprNode filter, AliasDerefMode aliasDerefMode,
        String... returningAttributes ) throws LdapException
    {
        OperationManager operationManager = directoryService.getOperationManager();

        SearchOperationContext searchContext = new SearchOperationContext( this, dn, scope, filter, returningAttributes );
        searchContext.setAliasDerefMode( aliasDerefMode );

        return operationManager.search( searchContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAnonymous()
    {
        if ( ( authorizedPrincipal == null ) && ( authenticatedPrincipal == null ) )
        {
            return true;
        }
        else
        {
            return authenticatedPrincipal.getAuthenticationLevel() == AuthenticationLevel.NONE;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean compare( CompareRequest compareRequest ) throws LdapException
    {
        CompareOperationContext compareContext = new CompareOperationContext( this, compareRequest );
        OperationManager operationManager = directoryService.getOperationManager();
        boolean result = false;
        try
        {
            result = operationManager.compare( compareContext );
        }
        catch ( LdapException e )
        {
            compareRequest.getResultResponse().addAllControls( compareContext.getResponseControls() );
            throw e;
        }

        compareRequest.getResultResponse().addAllControls( compareContext.getResponseControls() );
        return result;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( DeleteRequest deleteRequest ) throws LdapException
    {
        delete( deleteRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( DeleteRequest deleteRequest, LogChange log ) throws LdapException
    {
        DeleteOperationContext deleteContext = new DeleteOperationContext( this, deleteRequest );

        deleteContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        try
        {
            operationManager.delete( deleteContext );
        }
        catch ( LdapException e )
        {
            deleteRequest.getResultResponse().addAllControls( deleteContext.getResponseControls() );
            throw e;
        }

        deleteRequest.getResultResponse().addAllControls( deleteContext.getResponseControls() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean exists( String dn ) throws LdapException
    {
        return exists( new Dn( dn ) );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean exists( Dn dn ) throws LdapException
    {
        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( this, dn );
        OperationManager operationManager = directoryService.getOperationManager();

        return operationManager.hasEntry( hasEntryContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( ModifyRequest modifyRequest ) throws LdapException
    {
        modify( modifyRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( ModifyRequest modifyRequest, LogChange log ) throws LdapException
    {
        ModifyOperationContext modifyContext = new ModifyOperationContext( this, modifyRequest );

        modifyContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        try
        {
            operationManager.modify( modifyContext );
        }
        catch ( LdapException e )
        {
            modifyRequest.getResultResponse().addAllControls( modifyContext.getResponseControls() );
            throw e;
        }

        modifyRequest.getResultResponse().addAllControls( modifyContext.getResponseControls() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( ModifyDnRequest modifyDnRequest ) throws LdapException
    {
        move( modifyDnRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void move( ModifyDnRequest modifyDnRequest, LogChange log ) throws LdapException
    {
        MoveOperationContext moveContext = new MoveOperationContext( this, modifyDnRequest );

        moveContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        try
        {
            operationManager.move( moveContext );
        }
        catch ( LdapException e )
        {
            modifyDnRequest.getResultResponse().addAllControls( moveContext.getResponseControls() );
            throw e;
        }

        modifyDnRequest.getResultResponse().addAllControls( moveContext.getResponseControls() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( ModifyDnRequest modifyDnRequest ) throws LdapException
    {
        moveAndRename( modifyDnRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( ModifyDnRequest modifyDnRequest, LogChange log ) throws LdapException
    {
        MoveAndRenameOperationContext moveAndRenameContext = new MoveAndRenameOperationContext( this, modifyDnRequest );

        moveAndRenameContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        try
        {
            operationManager.moveAndRename( moveAndRenameContext );
        }
        catch ( LdapException e )
        {
            modifyDnRequest.getResultResponse().addAllControls( moveAndRenameContext.getResponseControls() );
            throw e;
        }

        modifyDnRequest.getResultResponse().addAllControls( moveAndRenameContext.getResponseControls() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( ModifyDnRequest modifyDnRequest ) throws LdapException
    {
        rename( modifyDnRequest, LogChange.TRUE );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( ModifyDnRequest modifyDnRequest, LogChange log ) throws LdapException
    {
        RenameOperationContext renameContext = new RenameOperationContext( this, modifyDnRequest );

        renameContext.setLogChange( log );

        OperationManager operationManager = directoryService.getOperationManager();

        try
        {
            operationManager.rename( renameContext );
        }
        catch ( LdapException e )
        {
            modifyDnRequest.getResultResponse().addAllControls( renameContext.getResponseControls() );
            throw e;
        }

        modifyDnRequest.getResultResponse().addAllControls( renameContext.getResponseControls() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Cursor<Entry> search( SearchRequest searchRequest ) throws LdapException
    {
        SearchOperationContext searchContext = new SearchOperationContext( this, searchRequest );
        searchContext.setSyncreplSearch( searchRequest.getControls().containsKey( SyncRequestValue.OID ) );

        OperationManager operationManager = directoryService.getOperationManager();

        // Check if we received serverside sort Control
        SortRequest sortControl = ( SortRequest ) searchRequest.getControls().get( SortRequest.OID );

        SortResponse sortRespCtrl = null;

        ResultResponse done = searchRequest.getResultResponse();

        LdapResult ldapResult = done.getLdapResult();

        if ( sortControl != null )
        {
            sortRespCtrl = canSort( sortControl, ldapResult, getDirectoryService().getSchemaManager() );

            if ( sortControl.isCritical() && ( sortRespCtrl.getSortResult() != SortResultCode.SUCCESS ) )
            {
                ldapResult.setResultCode( ResultCodeEnum.UNAVAILABLE_CRITICAL_EXTENSION );
                done.addControl( sortRespCtrl );

                return new EmptyCursor<>();
            }
        }

        Cursor<Entry> cursor = null;

        try
        {
            cursor = operationManager.search( searchContext );

            if ( ( sortRespCtrl != null ) && ( sortRespCtrl.getSortResult() == SortResultCode.SUCCESS ) )
            {
                cursor = sortResults( cursor, sortControl, getDirectoryService().getSchemaManager() );
            }

            // the below condition is to satisfy the scenario 6 in section 2 of rfc2891
            if ( sortRespCtrl != null )
            {
                cursor.beforeFirst();

                if ( !cursor.next() )
                {
                    sortRespCtrl = null;
                }
                else
                {
                    // move the cursor back
                    cursor.previous();
                }
            }
        }
        catch ( LdapException e )
        {
            done.addAllControls( searchContext.getResponseControls() );
            throw e;
        }
        catch ( Exception e )
        {
            done.addAllControls( searchContext.getResponseControls() );
            throw new LdapException( e );
        }
        finally
        {
            // Don't close the transaction !!!
            LOG.debug( "Search done, the transaction is still opened" );
        }

        if ( sortRespCtrl != null )
        {
            done.addControl( sortRespCtrl );
        }

        done.addAllControls( searchContext.getResponseControls() );

        return cursor;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void unbind() throws LdapException
    {
        UnbindOperationContext unbindContext = new UnbindOperationContext( this );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.unbind( unbindContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void unbind( UnbindRequest unbindRequest ) throws LdapException
    {
        UnbindOperationContext unbindContext = new UnbindOperationContext( this, unbindRequest );

        OperationManager operationManager = directoryService.getOperationManager();
        operationManager.unbind( unbindContext );
    }


    /**
     * Checks if the requested search results can be sorted
     * 
     * @param sortControl the sort control
     * @param ldapResult the refrence to the LDAP result of the ongoing search operation
     * @param session the current session
     * @return a sort response control
     */
    private SortResponse canSort( SortRequest sortControl, LdapResult ldapResult, SchemaManager schemaManager )
    {
        SortResponse resp = new SortResponseImpl();

        List<SortKey> keys = sortControl.getSortKeys();

        // only ONE key is supported by the server for now
        if ( keys.size() > 1 )
        {
            ldapResult.setDiagnosticMessage( "Cannot sort results based on more than one attribute" );
            resp.setSortResult( SortResultCode.UNWILLINGTOPERFORM );
            return resp;
        }

        SortKey sk = keys.get( 0 );

        AttributeType at = schemaManager.getAttributeType( sk.getAttributeTypeDesc() );

        if ( at == null )
        {
            ldapResult.setDiagnosticMessage( "No attribute with the name " + sk.getAttributeTypeDesc()
                + " exists in the server's schema" );
            resp.setSortResult( SortResultCode.NOSUCHATTRIBUTE );
            resp.setAttributeName( sk.getAttributeTypeDesc() );
            return resp;
        }

        String mrOid = sk.getMatchingRuleId();

        if ( mrOid != null )
        {
            MatchingRule mr = at.getOrdering();

            if ( ( mr != null ) && ( !mrOid.equals( mr.getOid() ) ) )
            {
                ldapResult.setDiagnosticMessage( "Given matchingrule " + mrOid
                    + " is not applicable for the attribute " + sk.getAttributeTypeDesc() );
                resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
                resp.setAttributeName( sk.getAttributeTypeDesc() );
                return resp;
            }

            try
            {
                schemaManager.lookupComparatorRegistry( mrOid );
            }
            catch ( LdapException e )
            {
                ldapResult.setDiagnosticMessage( "Given matchingrule " + mrOid + " is not supported" );
                resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
                resp.setAttributeName( sk.getAttributeTypeDesc() );
                return resp;
            }
        }
        else
        {
            MatchingRule mr = at.getOrdering();

            if ( mr == null )
            {
                mr = at.getEquality();
            }

            ldapResult.setDiagnosticMessage( "Matchingrule is required for sorting by the attribute "
                + sk.getAttributeTypeDesc() );
            resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
            resp.setAttributeName( sk.getAttributeTypeDesc() );

            if ( mr == null )
            {
                return resp;
            }

            try
            {
                schemaManager.lookupComparatorRegistry( mr.getOid() );
            }
            catch ( LdapException e )
            {
                return resp;
            }
        }

        resp.setSortResult( SortResultCode.SUCCESS );

        return resp;
    }


    /**
     * Sorts the entries based on the given sortkey and returns the cursor
     * 
     * @param unsortedEntries the cursor containing un-sorted entries
     * @param control the sort control
     * @param schemaManager schema manager
     * @return a cursor containing sorted entries
     * @throws CursorException
     * @throws LdapException
     * @throws IOException
     * @throws KeyNotFoundException 
     */
    private Cursor<Entry> sortResults( Cursor<Entry> unsortedEntries, SortRequest control, SchemaManager schemaManager )
        throws CursorException, LdapException, IOException
    {
        unsortedEntries.beforeFirst();

        Entry first = null;

        if ( unsortedEntries.next() )
        {
            first = unsortedEntries.get();
        }

        if ( !unsortedEntries.next() )
        {
            unsortedEntries.beforeFirst();

            return unsortedEntries;
        }

        SortKey sk = control.getSortKeys().get( 0 );

        AttributeType at = schemaManager.getAttributeType( sk.getAttributeTypeDesc() );

        SortedEntryComparator comparator = new SortedEntryComparator( at, sk.getMatchingRuleId(), sk.isReverseOrder(),
            schemaManager );

        SortedEntrySerializer keySerializer = new SortedEntrySerializer();
        SortedEntrySerializer.setSchemaManager( schemaManager );
        
        File file = null;
        
        try 
        {
            file = Files.createTempFile( "replica", ".sorted-data" ).toFile();    // see DIRSERVER-2007
        } 
        catch ( IOException e ) 
        {
            // see DIRSERVER-2091
            LOG.error( "Error creating temp file in directory {} for sorting: {}",  
                System.getProperty( "java.io.tmpdir" ),  e.getMessage(), e );
            throw e;
        }

        BaseRecordManager recMan = new BaseRecordManager( file.getAbsolutePath() );

        jdbm.btree.BTree<Entry, String> btree = new jdbm.btree.BTree<>( recMan, comparator, keySerializer, NullStringSerializer.INSTANCE );
        

        btree.insert( first, "", false );

        // at this stage the cursor will be _on_ the next element, so read it
        btree.insert( unsortedEntries.get(), "", false );

        while ( unsortedEntries.next() )
        {
            Entry entry = unsortedEntries.get();
            btree.insert( entry, "", false );
        }

        unsortedEntries.close();

        return new SortedEntryCursor( btree, recMan, file );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isPwdMustChange() 
    {
        return pwdMustChange;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void setPwdMustChange( boolean pwdMustChange ) 
    {
        this.pwdMustChange = pwdMustChange;
    }

    
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasSessionTransaction()
    {
        return hasSessionTransaction;
    }
    
    
    /**
     * {@inheritDoc}
     */
    @Override
    public long beginSessionTransaction()
    {
        hasSessionTransaction = true;
        
        return transactionId.getAndIncrement();
    }
    
    
    /**
     * {@inheritDoc}
     */
    @Override
    public void endSessionTransaction( boolean commit ) throws IOException
    {
        if ( commit )
        {
            for ( Map.Entry<String, PartitionTxn> partitionTxn : transactionMap.entrySet() )
            {
                partitionTxn.getValue().commit();
            }
        }
        else
        {
            for ( Map.Entry<String, PartitionTxn> partitionTxn : transactionMap.entrySet() )
            {
                partitionTxn.getValue().abort();
            }
        }
        
        hasSessionTransaction = false;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public PartitionTxn getTransaction( Partition partition ) 
    {
        return transactionMap.get( partition.getId() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void addTransaction( Partition partition, PartitionTxn transaction )
    {
        if ( !transactionMap.containsKey( partition.getId() ) )
        {
            transactionMap.put( partition.getId(), transaction );
        }
    }
}
