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


import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.directory.api.ldap.extras.controls.ad.TreeDelete;
import org.apache.directory.api.ldap.model.constants.Loggers;
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.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapAffectMultipleDsaException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapNoSuchObjectException;
import org.apache.directory.api.ldap.model.exception.LdapOperationErrorException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
import org.apache.directory.api.ldap.model.exception.LdapPartialResultException;
import org.apache.directory.api.ldap.model.exception.LdapReferralException;
import org.apache.directory.api.ldap.model.exception.LdapServiceUnavailableException;
import org.apache.directory.api.ldap.model.exception.LdapURLEncodingException;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.SearchScope;
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.url.LdapUrl;
import org.apache.directory.server.constants.ApacheSchemaConstants;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.OperationManager;
import org.apache.directory.server.core.api.ReferralManager;
import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
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.GetRootDseOperationContext;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * The default implementation of an OperationManager.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class DefaultOperationManager implements OperationManager
{
    /** A logger specifically for operations */
    private static final Logger OPERATION_LOG = LoggerFactory.getLogger( Loggers.OPERATION_LOG.getName() );

    /** A logger specifically for operations time */
    private static final Logger OPERATION_TIME = LoggerFactory.getLogger( Loggers.OPERATION_TIME.getName() );

    /** A logger specifically for operations statistics */
    private static final Logger OPERATION_STAT = LoggerFactory.getLogger( Loggers.OPERATION_STAT.getName() );

    /** Speedup for logs */
    private static final boolean IS_DEBUG = OPERATION_LOG.isDebugEnabled();
    private static final boolean IS_TIME = OPERATION_TIME.isDebugEnabled();
    private static final boolean IS_STAT = OPERATION_STAT.isDebugEnabled();

    /** The directory service instance */
    private final DirectoryService directoryService;

    /** A lock used to protect against concurrent operations */
    private ReadWriteLock rwLock = new ReentrantReadWriteLock( true );

    /** A reference to the ObjectClass AT */
    protected AttributeType objectClassAT;
    
    /** The nbChildren count attributeType */
    protected AttributeType nbChildrenAT;
    
    public DefaultOperationManager( DirectoryService directoryService )
    {
        this.directoryService = directoryService;
    }


    /**
     * {@inheritDoc}
     */
    public ReadWriteLock getRWLock()
    {
        return rwLock;
    }


    /**
     * Acquires a ReadLock
     */
    public void lockRead()
    {
        rwLock.readLock().lock();
    }


    /**
     * Acquires a WriteLock
     */
    public void lockWrite()
    {
        rwLock.writeLock().lock();
    }


    /**
     * Releases a WriteLock
     */
    public void unlockWrite()
    {
        rwLock.writeLock().unlock();
    }


    /**
     * Releases a ReadLock
     */
    public void unlockRead()
    {
        rwLock.readLock().unlock();
    }


    /**
     * Eagerly populates fields of operation contexts so multiple Interceptors
     * in the processing pathway can reuse this value without performing a
     * redundant lookup operation.
     *
     * @param opContext the operation context to populate with cached fields
     */
    private void eagerlyPopulateFields( OperationContext opContext ) throws LdapException
    {
        // If the entry field is not set for ops other than add for example
        // then we set the entry but don't freak if we fail to do so since it
        // may not exist in the first place

        if ( opContext.getEntry() == null )
        {
            // We have to use the admin session here, otherwise we may have
            // trouble reading the entry due to insufficient access rights
            CoreSession adminSession = opContext.getSession().getDirectoryService().getAdminSession();

            LookupOperationContext lookupContext = new LookupOperationContext( adminSession, opContext.getDn(),
                SchemaConstants.ALL_ATTRIBUTES_ARRAY );
            lookupContext.setPartition( opContext.getPartition() );
            lookupContext.setTransaction( opContext.getTransaction() );
            Entry foundEntry = opContext.getSession().getDirectoryService().getPartitionNexus().lookup( lookupContext );

            if ( foundEntry != null )
            {
                opContext.setEntry( foundEntry );
            }
            else
            {
                // This is an error : we *must* have an entry if we want to be able to rename.
                throw new LdapNoSuchObjectException( I18n.err( I18n.ERR_256_NO_SUCH_OBJECT, opContext.getDn() ) );
            }
        }
    }


    private Entry getOriginalEntry( OperationContext opContext ) throws LdapException
    {
        // We have to use the admin session here, otherwise we may have
        // trouble reading the entry due to insufficient access rights
        CoreSession adminSession = opContext.getSession().getDirectoryService().getAdminSession();

        Entry foundEntry = adminSession.lookup( opContext.getDn(), SchemaConstants.ALL_OPERATIONAL_ATTRIBUTES,
            SchemaConstants.ALL_USER_ATTRIBUTES );

        if ( foundEntry != null )
        {
            return foundEntry;
        }
        else
        {
            // This is an error : we *must* have an entry if we want to be able to rename.
            throw new LdapNoSuchObjectException( I18n.err( I18n.ERR_256_NO_SUCH_OBJECT,
                opContext.getDn() ) );
        }
    }


    private LdapReferralException buildReferralException( Entry parentEntry, Dn childDn ) throws LdapException
    {
        // Get the Ref attributeType
        Attribute refs = parentEntry.get( SchemaConstants.REF_AT );

        List<String> urls = new ArrayList<>();

        try
        {
            // manage each Referral, building the correct URL for each of them
            for ( Value url : refs )
            {
                // we have to replace the parent by the referral
                LdapUrl ldapUrl = new LdapUrl( url.getString() );

                // We have a problem with the Dn : we can't use the UpName,
                // as we may have some spaces around the ',' and '+'.
                // So we have to take the Rdn one by one, and create a
                // new Dn with the type and value UP form

                Dn urlDn = ldapUrl.getDn().add( childDn );

                ldapUrl.setDn( urlDn );
                urls.add( ldapUrl.toString() );
            }
        }
        catch ( LdapURLEncodingException luee )
        {
            throw new LdapOperationErrorException( luee.getMessage(), luee );
        }

        // Return with an exception
        LdapReferralException lre = new LdapReferralException( urls );
        lre.setRemainingDn( childDn );
        lre.setResolvedDn( parentEntry.getDn() );
        lre.setResolvedObject( parentEntry );

        return lre;
    }


    private LdapReferralException buildReferralExceptionForSearch( Entry parentEntry, Dn childDn, SearchScope scope )
        throws LdapException
    {
        // Get the Ref attributeType
        Attribute refs = parentEntry.get( SchemaConstants.REF_AT );

        List<String> urls = new ArrayList<>();

        // manage each Referral, building the correct URL for each of them
        for ( Value url : refs )
        {
            // we have to replace the parent by the referral
            try
            {
                LdapUrl ldapUrl = new LdapUrl( url.getString() );

                StringBuilder urlString = new StringBuilder();

                if ( ( ldapUrl.getDn() == null ) || ( ldapUrl.getDn() == Dn.ROOT_DSE ) )
                {
                    ldapUrl.setDn( parentEntry.getDn() );
                }
                else
                {
                    // We have a problem with the Dn : we can't use the UpName,
                    // as we may have some spaces around the ',' and '+'.
                    // So we have to take the Rdn one by one, and create a
                    // new Dn with the type and value UP form

                    Dn urlDn = ldapUrl.getDn().add( childDn );

                    ldapUrl.setDn( urlDn );
                }

                urlString.append( ldapUrl.toString() ).append( "??" );

                switch ( scope )
                {
                    case OBJECT:
                        urlString.append( "base" );
                        break;

                    case SUBTREE:
                        urlString.append( "sub" );
                        break;

                    case ONELEVEL:
                        urlString.append( "one" );
                        break;

                    default:
                        throw new IllegalArgumentException( "Unexpected scope " + scope );
                }

                urls.add( urlString.toString() );
            }
            catch ( LdapURLEncodingException luee )
            {
                // The URL is not correct, returns it as is
                urls.add( url.getString() );
            }
        }

        // Return with an exception
        LdapReferralException lre = new LdapReferralException( urls );
        lre.setRemainingDn( childDn );
        lre.setResolvedDn( parentEntry.getDn() );
        lre.setResolvedObject( parentEntry );

        return lre;
    }


    private LdapPartialResultException buildLdapPartialResultException( Dn childDn )
    {
        LdapPartialResultException lpre = new LdapPartialResultException( I18n.err( I18n.ERR_315 ) );

        lpre.setRemainingDn( childDn );
        lpre.setResolvedDn( Dn.EMPTY_DN );

        return lpre;
    }


    /**
     * {@inheritDoc}
     */
    public void add( AddOperationContext addContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> AddOperation : {}", addContext );
        }

        long addStart = 0L;

        if ( IS_TIME )
        {
            addStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the addContext Dn
        Dn dn = addContext.getDn();
        
        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            addContext.setDn( dn );
        }
        
        // Find the working partition
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        addContext.setPartition( partition );
        
        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            if ( directoryService.getReferralManager().hasParentReferral( dn ) )
            {
                Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                // Depending on the Context.REFERRAL property value, we will throw
                // a different exception.
                if ( addContext.isReferralIgnored() )
                {
                    throw buildLdapPartialResultException( childDn );
                }
                else
                {
                    throw buildReferralException( parentEntry, childDn );
                }
            }
        }
        finally
        {
            // Unlock the referral manager
            directoryService.getReferralManager().unlock();
        }

        // Call the Add method
        Interceptor head = directoryService.getInterceptor( addContext.getNextInterceptor() );

        lockWrite();

        // Start a Write transaction right away
        PartitionTxn transaction = addContext.getSession().getTransaction( partition ); 
        
        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( addContext.getSession().hasSessionTransaction() )
                {
                    addContext.getSession().addTransaction( partition, transaction );
                }
            }
            
            addContext.setTransaction( transaction );

            head.add( addContext );
            
            if ( !addContext.getSession().hasSessionTransaction() )
            {
                transaction.commit();
            }
        }
        catch ( LdapException le )
        {
            try
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try
            {
                transaction.abort();
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< AddOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Add operation took {} ns", ( System.nanoTime() - addStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public void bind( BindOperationContext bindContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> BindOperation : {}", bindContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Call the Delete method
        Interceptor head = directoryService.getInterceptor( bindContext.getNextInterceptor() );

        // Normalize the addContext Dn
        Dn dn = bindContext.getDn();
        
        if ( ( dn != null ) && !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            bindContext.setDn( dn );
        }

        lockRead();

        try
        {
            Partition partition = directoryService.getPartitionNexus().getPartition( dn );
            
            try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
            {
                bindContext.setPartition( partition );
                bindContext.setTransaction( partitionTxn );
                
                head.bind( bindContext );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        finally
        {
            unlockRead();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< BindOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Bind operation took {} ns", ( System.nanoTime() - opStart )  );
        }
    }


    /**
     * {@inheritDoc}
     */
    public boolean compare( CompareOperationContext compareContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> CompareOperation : {}", compareContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();
        
        // Normalize the compareContext Dn
        Dn dn = compareContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            compareContext.setDn( dn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !compareContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( compareContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }
        }
        finally
        {
            // Unlock the ReferralManager
            directoryService.getReferralManager().unlock();
        }

        // populate the context with the old entry
        compareContext.setOriginalEntry( getOriginalEntry( compareContext ) );

        // Call the Compare method
        Interceptor head = directoryService.getInterceptor( compareContext.getNextInterceptor() );

        boolean result = false;

        lockRead();

        try
        {
            Partition partition = directoryService.getPartitionNexus().getPartition( dn );
            
            try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
            {
                compareContext.setPartition( partition );
                compareContext.setTransaction( partitionTxn );
                
                result = head.compare( compareContext );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        finally
        {
            unlockRead();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< CompareOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Compare operation took {} ns", ( System.nanoTime() - opStart ) );
        }

        return result;
    }
    
    
    private void deleteEntry( DeleteOperationContext deleteContext, Dn dn ) throws LdapException
    {
        DeleteOperationContext entryDeleteContext = 
            new DeleteOperationContext( deleteContext.getSession(), dn );
        entryDeleteContext.setTransaction( deleteContext.getTransaction() );

        eagerlyPopulateFields( entryDeleteContext );
        
        // Call the Delete method
        Interceptor head = directoryService.getInterceptor( deleteContext.getNextInterceptor() );

        head.delete( entryDeleteContext );
    }
    
    
    private void processTreeDelete( DeleteOperationContext deleteContext, Dn dn ) throws LdapException, CursorException
    {
        objectClassAT = directoryService.getSchemaManager().getAttributeType( SchemaConstants.OBJECT_CLASS_AT );
        nbChildrenAT = directoryService.getSchemaManager().getAttributeType( ApacheSchemaConstants.NB_CHILDREN_OID );

        // This is a depth first recursive operation
        PresenceNode filter = new PresenceNode( objectClassAT );
        SearchOperationContext searchContext = new SearchOperationContext( 
            deleteContext.getSession(), 
            dn, 
            SearchScope.ONELEVEL, filter,
            ApacheSchemaConstants.NB_CHILDREN_OID );
        searchContext.setTransaction( deleteContext.getTransaction() );

        EntryFilteringCursor cursor = search( searchContext );
        
        cursor.beforeFirst();
        
        while ( cursor.next() )
        {
            Entry entry = cursor.get();
            
            if ( Integer.parseInt( entry.get( nbChildrenAT ).getString() ) == 0 )
            {
                // We can delete the entry
                deleteEntry( deleteContext, entry.getDn() );
            }
            else
            {
                // Recurse
                processTreeDelete( deleteContext, entry.getDn() );
            }
        }
        
        // Done with the children, we can delete the entry
        // We can delete the entry
        deleteEntry( deleteContext, dn );
    }


    /**
     * {@inheritDoc}
     */
    public void delete( DeleteOperationContext deleteContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> DeleteOperation : {}", deleteContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the deleteContext Dn
        Dn dn = deleteContext.getDn();
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        deleteContext.setPartition( partition );

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            deleteContext.setDn( dn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !deleteContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // We can't delete an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( deleteContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }
        }
        finally
        {
            // Unlock the ReferralManager
            directoryService.getReferralManager().unlock();
        }

        // populate the context with the old entry
        lockWrite();

        // Start a Write transaction right away
        PartitionTxn transaction = deleteContext.getSession().getTransaction( partition ); 
        
        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( deleteContext.getSession().hasSessionTransaction() )
                {
                    deleteContext.getSession().addTransaction( partition, transaction );
                }
            }
            
            deleteContext.setTransaction( transaction );

            // Check if the TreeDelete control is used
            if ( deleteContext.hasRequestControl( TreeDelete.OID ) )
            {
                try
                {
                    processTreeDelete( deleteContext, deleteContext.getDn() );

                    if ( !deleteContext.getSession().hasSessionTransaction() )
                    {
                        transaction.commit();
                    }
                }
                catch ( CursorException ce )
                {
                    try
                    {
                        if ( transaction != null )
                        {
                            transaction.abort();
                        }
                        
                        throw new LdapOtherException( ce.getMessage(), ce );
                    }
                    catch ( IOException ioe )
                    {
                        throw new LdapOtherException( ioe.getMessage(), ioe );
                    }
                }
            }
            else
            {
                eagerlyPopulateFields( deleteContext );
    
                // Call the Delete method
                Interceptor head = directoryService.getInterceptor( deleteContext.getNextInterceptor() );
    
                head.delete( deleteContext );
    
                if ( !deleteContext.getSession().hasSessionTransaction() )
                {
                    transaction.commit();
                }
            }
        }
        catch ( LdapException le )
        {
            try
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try
            {
                transaction.abort();
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< DeleteOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Delete operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public Entry getRootDse( GetRootDseOperationContext getRootDseContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> GetRootDseOperation : {}", getRootDseContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        Interceptor head = directoryService.getInterceptor( getRootDseContext.getNextInterceptor() );
        Entry root;

        try
        {
            lockRead();
            
            Partition partition = directoryService.getPartitionNexus().getPartition( Dn.ROOT_DSE );
            
            try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
            {
                getRootDseContext.setPartition( partition );
                getRootDseContext.setTransaction( partitionTxn );
                
                root = head.getRootDse( getRootDseContext );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        finally
        {
            unlockRead();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< getRootDseOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "GetRootDSE operation took {} ns", ( System.nanoTime() - opStart ) );
        }

        return root;
    }


    /**
     * {@inheritDoc}
     */
    public boolean hasEntry( HasEntryOperationContext hasEntryContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> hasEntryOperation : {}", hasEntryContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        Interceptor head = directoryService.getInterceptor( hasEntryContext.getNextInterceptor() );

        boolean result = false;

        lockRead();

        // Normalize the addContext Dn
        Dn dn = hasEntryContext.getDn();
        
        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            hasEntryContext.setDn( dn );
        }

        try
        {
            Partition partition = directoryService.getPartitionNexus().getPartition( dn );

            try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
            {
                hasEntryContext.setPartition( partition );
                hasEntryContext.setTransaction( partitionTxn );

                result = head.hasEntry( hasEntryContext );
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        finally
        {
            unlockRead();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< HasEntryOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "HasEntry operation took {} ns", ( System.nanoTime() - opStart ) );
        }

        return result;
    }


    /**
     * {@inheritDoc}
     */
    public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> LookupOperation : {}", lookupContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        Interceptor head = directoryService.getInterceptor( lookupContext.getNextInterceptor() );

        Entry entry = null;

        // Normalize the modifyContext Dn
        Dn dn = lookupContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            lookupContext.setDn( dn );
        }
        
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        lookupContext.setPartition( partition );
        
        // Start a read transaction right away
        try ( PartitionTxn transaction = partition.beginReadTransaction() )
        {
            lookupContext.setTransaction( transaction );

            lockRead();
    
            try
            {
                entry = head.lookup( lookupContext );
            }
            finally
            {
                unlockRead();
            }
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< LookupOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Lookup operation took {} ns", ( System.nanoTime() - opStart ) );
        }

        return entry;
    }


    /**
     * {@inheritDoc}
     */
    public void modify( ModifyOperationContext modifyContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> ModifyOperation : {}", modifyContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the modifyContext Dn
        Dn dn = modifyContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            modifyContext.setDn( dn );
        }

        ReferralManager referralManager = directoryService.getReferralManager();

        // We have to deal with the referral first
        referralManager.lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = referralManager.getParentReferral( dn );

            if ( parentEntry != null )
            {
                if ( referralManager.isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !modifyContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        // We have found a parent referral for the current Dn
                        Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( referralManager.hasParentReferral( dn ) )
                {
                    // We can't delete an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( modifyContext.isReferralIgnored() )
                    {
                        // We have found a parent referral for the current Dn
                        Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        // We have found a parent referral for the current Dn
                        Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }
        }
        finally
        {
            // Unlock the ReferralManager
            referralManager.unlock();
        }
        
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        modifyContext.setPartition( partition );
        
        lockWrite();
        
        // Start a Write transaction right away
        PartitionTxn transaction = modifyContext.getSession().getTransaction( partition ); 

        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( modifyContext.getSession().hasSessionTransaction() )
                {
                    modifyContext.getSession().addTransaction( partition, transaction );
                }
            }

            modifyContext.setTransaction( transaction );

            // populate the context with the old entry
            eagerlyPopulateFields( modifyContext );

            // Call the Modify method
            Interceptor head = directoryService.getInterceptor( modifyContext.getNextInterceptor() );

            head.modify( modifyContext );
            
            if ( !modifyContext.getSession().hasSessionTransaction() )
            {
                transaction.commit();
            }
        }
        catch ( LdapException le )
        {
            try 
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try 
            {
                transaction.abort();
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< ModifyOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Modify operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public void move( MoveOperationContext moveContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> MoveOperation : {}", moveContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the moveContext Dn
        Dn dn = moveContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            moveContext.setDn( dn );
        }

        // Normalize the moveContext superior Dn
        Dn newSuperiorDn = moveContext.getNewSuperior();

        if ( !newSuperiorDn.isSchemaAware() )
        {
            newSuperiorDn = new Dn( directoryService.getSchemaManager(), newSuperiorDn );
            moveContext.setNewSuperior( newSuperiorDn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !moveContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // We can't delete an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( moveContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }

            // Now, check the destination
            // If he parent Dn is a referral, or has a referral ancestor, we have to issue a AffectMultipleDsas result
            // as stated by RFC 3296 Section 5.6.2
            if ( directoryService.getReferralManager().isReferral( newSuperiorDn )
                || directoryService.getReferralManager().hasParentReferral( newSuperiorDn ) )
            {
                throw new LdapAffectMultipleDsaException();
            }

        }
        finally
        {
            // Unlock the referral manager
            directoryService.getReferralManager().unlock();
        }

        lockWrite();
        
        // Find the working partition
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        moveContext.setPartition( partition );

        // Start a Write transaction right away
        PartitionTxn transaction = moveContext.getSession().getTransaction( partition ); 
        
        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( moveContext.getSession().hasSessionTransaction() )
                {
                    moveContext.getSession().addTransaction( partition, transaction );
                }
            }
        
            moveContext.setTransaction( transaction );
            Entry originalEntry = getOriginalEntry( moveContext );

            moveContext.setOriginalEntry( originalEntry );

            // Call the Move method
            Interceptor head = directoryService.getInterceptor( moveContext.getNextInterceptor() );

            head.move( moveContext );
            
            if ( !moveContext.getSession().hasSessionTransaction() )
            {
                transaction.commit();
            }
        }
        catch ( LdapException le )
        {
            try
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< MoveOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Move operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> MoveAndRenameOperation : {}", moveAndRenameContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the moveAndRenameContext Dn
        Dn dn = moveAndRenameContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            moveAndRenameContext.setDn( dn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !moveAndRenameContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // We can't delete an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( moveAndRenameContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }

            // Now, check the destination
            // Normalize the moveAndRenameContext Dn
            Dn newSuperiorDn = moveAndRenameContext.getNewSuperiorDn();

            if ( !newSuperiorDn.isSchemaAware() )
            {
                newSuperiorDn = new Dn( directoryService.getSchemaManager(), newSuperiorDn );
                moveAndRenameContext.setNewSuperiorDn( newSuperiorDn );
            }

            // If he parent Dn is a referral, or has a referral ancestor, we have to issue a AffectMultipleDsas result
            // as stated by RFC 3296 Section 5.6.2
            if ( directoryService.getReferralManager().isReferral( newSuperiorDn )
                || directoryService.getReferralManager().hasParentReferral( newSuperiorDn ) )
            {
                // The parent Dn is a referral, we have to issue a AffectMultipleDsas result
                // as stated by RFC 3296 Section 5.6.2
                throw new LdapAffectMultipleDsaException();
            }
        }
        finally
        {
            // Unlock the ReferralManager
            directoryService.getReferralManager().unlock();
        }

        // Find the working partition
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        moveAndRenameContext.setPartition( partition );

        lockWrite();
        
        // Start a Write transaction right away
        PartitionTxn transaction = moveAndRenameContext.getSession().getTransaction( partition ); 
        
        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( moveAndRenameContext.getSession().hasSessionTransaction() )
                {
                    moveAndRenameContext.getSession().addTransaction( partition, transaction );
                }
            }

            moveAndRenameContext.setOriginalEntry( getOriginalEntry( moveAndRenameContext ) );
            moveAndRenameContext.setModifiedEntry( moveAndRenameContext.getOriginalEntry().clone() );
            moveAndRenameContext.setTransaction( transaction );

            // Call the MoveAndRename method
            Interceptor head = directoryService.getInterceptor( moveAndRenameContext.getNextInterceptor() );

            head.moveAndRename( moveAndRenameContext );

            if ( !moveAndRenameContext.getSession().hasSessionTransaction() )
            {
                transaction.commit();
            }
        }
        catch ( LdapException le )
        {
            try
            {
                if ( transaction != null )
                {
                    transaction.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try
            {
                transaction.abort();
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< MoveAndRenameOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "MoveAndRename operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public void rename( RenameOperationContext renameContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> RenameOperation : {}", renameContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the renameContext Dn
        Dn dn = renameContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            renameContext.setDn( dn );
        }

        // Inject the newDn into the operation context
        // Inject the new Dn into the context
        if ( !dn.isEmpty() )
        {
            Dn newDn = dn.getParent();
            Rdn newRdn = renameContext.getNewRdn();
            
            if ( !newRdn.isSchemaAware() )
            {
                newRdn = new Rdn( directoryService.getSchemaManager(), newRdn );
                renameContext.setNewRdn( newRdn );
            }
            
            newDn = newDn.add( renameContext.getNewRdn() );
            renameContext.setNewDn( newDn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can delete it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !renameContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // We can't delete an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( renameContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralException( parentEntry, childDn );
                    }
                }
            }
        }
        finally
        {
            // Unlock the ReferralManager
            directoryService.getReferralManager().unlock();
        }

        lockWrite();

        Partition partition = directoryService.getPartitionNexus().getPartition( dn );

        // Start a Write transaction right away
        PartitionTxn transaction = renameContext.getSession().getTransaction( partition ); 
        
        // Call the rename method
        try
        {
            if ( transaction == null )
            {
                transaction = partition.beginWriteTransaction();
                
                if ( renameContext.getSession().hasSessionTransaction() )
                {
                    renameContext.getSession().addTransaction( partition, transaction );
                }
            }

            renameContext.setPartition( partition );

            // populate the context with the old entry
            PartitionTxn partitionTxn = null;
            
            try
            {
                partitionTxn = partition.beginReadTransaction();
                
                renameContext.setTransaction( partitionTxn );
                
                eagerlyPopulateFields( renameContext );
            }
            finally
            {
                try
                {
                    // Nothing to do
                    if ( partitionTxn != null )
                    {
                        partitionTxn.close();
                    }
                }
                catch ( IOException ioe )
                {
                    throw new LdapOtherException( ioe.getMessage(), ioe );
                }
            }

            Entry originalEntry = getOriginalEntry( renameContext );
            renameContext.setOriginalEntry( originalEntry );
            renameContext.setModifiedEntry( originalEntry.clone() );
            Interceptor head = directoryService.getInterceptor( renameContext.getNextInterceptor() );

            // Start a Write transaction right away
            transaction = renameContext.getSession().getTransaction( partition ); 
            
            // Call the Rename method
            try
            {
                if ( transaction == null )
                {
                    transaction = partition.beginWriteTransaction();
                    
                    if ( renameContext.getSession().hasSessionTransaction() )
                    {
                        renameContext.getSession().addTransaction( partition, transaction );
                    }
                }

                renameContext.setTransaction( transaction );

                head.rename( renameContext );
                
                if ( !renameContext.getSession().hasSessionTransaction() )
                {
                    transaction.commit();
                }
            }
            catch ( LdapException le )
            {
                try
                {
                    if ( transaction != null )
                    {
                        transaction.abort();
                    }
                    
                    throw le;
                }
                catch ( IOException ioe )
                {
                    throw new LdapOtherException( ioe.getMessage(), ioe );
                }
            }
            catch ( IOException ioe )
            {
                try
                {
                    if ( transaction != null )
                    {
                        transaction.abort();
                    }
                    
                    throw new LdapOtherException( ioe.getMessage(), ioe );
                }
                catch ( IOException ioe2 )
                {
                    throw new LdapOtherException( ioe2.getMessage(), ioe2 );
                }
            }
        }
        finally
        {
            unlockWrite();
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< RenameOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Rename operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    /**
     * {@inheritDoc}
     */
    public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> SearchOperation : {}", searchContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Normalize the searchContext Dn
        Dn dn = searchContext.getDn();

        if ( !dn.isSchemaAware() )
        {
            dn = new Dn( directoryService.getSchemaManager(), dn );
            searchContext.setDn( dn );
        }

        // We have to deal with the referral first
        directoryService.getReferralManager().lockRead();

        try
        {
            // Check if we have an ancestor for this Dn
            Entry parentEntry = directoryService.getReferralManager().getParentReferral( dn );

            if ( parentEntry != null )
            {
                // We have found a parent referral for the current Dn
                Dn childDn = dn.getDescendantOf( parentEntry.getDn() );

                if ( directoryService.getReferralManager().isReferral( dn ) )
                {
                    // This is a referral. We can return it if the ManageDsaIt flag is true
                    // Otherwise, we just throw a LdapReferralException
                    if ( !searchContext.isReferralIgnored() )
                    {
                        // Throw a Referral Exception
                        throw buildReferralExceptionForSearch( parentEntry, childDn, searchContext.getScope() );
                    }
                }
                else if ( directoryService.getReferralManager().hasParentReferral( dn ) )
                {
                    // We can't search an entry which has an ancestor referral

                    // Depending on the Context.REFERRAL property value, we will throw
                    // a different exception.
                    if ( searchContext.isReferralIgnored() )
                    {
                        throw buildLdapPartialResultException( childDn );
                    }
                    else
                    {
                        throw buildReferralExceptionForSearch( parentEntry, childDn, searchContext.getScope() );
                    }
                }
            }
        }
        finally
        {
            // Unlock the ReferralManager
            directoryService.getReferralManager().unlock();
        }

        // Call the Search method
        Interceptor head = directoryService.getInterceptor( searchContext.getNextInterceptor() );

        EntryFilteringCursor cursor = null;
        Partition partition = directoryService.getPartitionNexus().getPartition( dn );
        
        try ( PartitionTxn partitionTxn = partition.beginReadTransaction() )
        {
            searchContext.setPartition( partition );
            searchContext.setTransaction( partitionTxn );
            lockRead();
    
            try
            {
                cursor = head.search( searchContext );
            }
            finally
            {
                unlockRead();
            }
        }
        catch ( IOException ioe )
        {
            throw new LdapOtherException( ioe.getMessage(), ioe );
        }

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< SearchOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Search operation took {} ns", ( System.nanoTime() - opStart ) );
        }

        return cursor;
    }


    /**
     * {@inheritDoc}
     */
    public void unbind( UnbindOperationContext unbindContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( ">> UnbindOperation : {}", unbindContext );
        }

        long opStart = 0L;

        if ( IS_TIME )
        {
            opStart = System.nanoTime();
        }

        ensureStarted();

        // Call the Unbind method
        Interceptor head = directoryService.getInterceptor( unbindContext.getNextInterceptor() );

        head.unbind( unbindContext );

        if ( IS_DEBUG )
        {
            OPERATION_LOG.debug( "<< UnbindOperation successful" );
        }

        if ( IS_TIME )
        {
            OPERATION_TIME.debug( "Unbind operation took {} ns", ( System.nanoTime() - opStart ) );
        }
    }


    private void ensureStarted() throws LdapServiceUnavailableException
    {
        if ( !directoryService.isStarted() )
        {
            throw new LdapServiceUnavailableException( ResultCodeEnum.UNAVAILABLE, I18n.err( I18n.ERR_316 ) );
        }
    }
}
