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


import java.io.Serializable;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NoPermissionException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.directory.DirContext;
import javax.naming.directory.SchemaViolationException;
import javax.naming.directory.SearchControls;
import javax.naming.event.EventContext;
import javax.naming.event.NamingListener;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapName;
import javax.naming.spi.DirStateFactory;
import javax.naming.spi.DirectoryManager;

import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.ldap.codec.api.CodecControl;
import org.apache.directory.api.ldap.codec.controls.cascade.CascadeDecorator;
import org.apache.directory.api.ldap.codec.controls.manageDsaIT.ManageDsaITDecorator;
import org.apache.directory.api.ldap.codec.controls.search.entryChange.EntryChangeDecorator;
import org.apache.directory.api.ldap.codec.controls.search.pagedSearch.PagedResultsDecorator;
import org.apache.directory.api.ldap.codec.controls.search.persistentSearch.PersistentSearchDecorator;
import org.apache.directory.api.ldap.codec.controls.search.subentries.SubentriesDecorator;
import org.apache.directory.api.ldap.extras.controls.SyncDoneValue;
import org.apache.directory.api.ldap.extras.controls.SyncInfoValue;
import org.apache.directory.api.ldap.extras.controls.SyncRequestValue;
import org.apache.directory.api.ldap.extras.controls.SyncStateValue;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicy;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyImpl;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponseImpl;
import org.apache.directory.api.ldap.extras.controls.ppolicy_impl.PasswordPolicyDecorator;
import org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncDoneValueDecorator;
import org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncInfoValueDecorator;
import org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncRequestValueDecorator;
import org.apache.directory.api.ldap.extras.controls.syncrepl_impl.SyncStateValueDecorator;
import org.apache.directory.api.ldap.model.constants.JndiPropertyConstants;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.EmptyCursor;
import org.apache.directory.api.ldap.model.cursor.SingletonCursor;
import org.apache.directory.api.ldap.model.entry.AttributeUtils;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
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.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeTypeException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.message.controls.Cascade;
import org.apache.directory.api.ldap.model.message.controls.CascadeImpl;
import org.apache.directory.api.ldap.model.message.controls.EntryChange;
import org.apache.directory.api.ldap.model.message.controls.ManageDsaIT;
import org.apache.directory.api.ldap.model.message.controls.ManageDsaITImpl;
import org.apache.directory.api.ldap.model.message.controls.PagedResults;
import org.apache.directory.api.ldap.model.message.controls.PersistentSearch;
import org.apache.directory.api.ldap.model.message.controls.Subentries;
import org.apache.directory.api.ldap.model.name.Ava;
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.SchemaManager;
import org.apache.directory.api.ldap.util.JndiUtils;
import org.apache.directory.api.util.Strings;
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.OperationEnum;
import org.apache.directory.server.core.api.OperationManager;
import org.apache.directory.server.core.api.entry.ServerEntryUtils;
import org.apache.directory.server.core.api.event.DirectoryListener;
import org.apache.directory.server.core.api.event.NotificationCriteria;
import org.apache.directory.server.core.api.filtering.BaseEntryFilteringCursor;
import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
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.shared.DefaultCoreSession;
import org.apache.directory.server.i18n.I18n;


/**
 * A non-federated abstract Context implementation.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public abstract class ServerContext implements EventContext
{
    /** property key used for deleting the old Rdn on a rename */
    public static final String DELETE_OLD_RDN_PROP = JndiPropertyConstants.JNDI_LDAP_DELETE_RDN;

    /** Empty array of controls for use in dealing with them */
    protected static final Control[] EMPTY_CONTROLS = new Control[0];

    /** The directory service which owns this context **/
    private final DirectoryService service;

    /** The SchemManager instance */
    protected SchemaManager schemaManager;

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

    /** The cloned environment used by this Context */
    private final Hashtable<String, Object> env;

    /** The distinguished name of this Context */
    private final Dn dn;

    /** The set of registered NamingListeners */
    private final Map<NamingListener, DirectoryListener> listeners =
        new HashMap<NamingListener, DirectoryListener>();

    /** The request controls to set on operations before performing them */
    protected Control[] requestControls = EMPTY_CONTROLS;

    /** The response controls to set after performing operations */
    protected Control[] responseControls = EMPTY_CONTROLS;

    /** Connection level controls associated with the session */
    protected Control[] connectControls = EMPTY_CONTROLS;

    /** The session */
    private final CoreSession session;

    private static final Map<String, ControlEnum> ADS_CONTROLS = new HashMap<String, ControlEnum>();

    static
    {
        ADS_CONTROLS.put( Cascade.OID, ControlEnum.CASCADE_CONTROL );
        ADS_CONTROLS.put( EntryChange.OID, ControlEnum.ENTRY_CHANGE_CONTROL );
        ADS_CONTROLS.put( ManageDsaIT.OID, ControlEnum.MANAGE_DSA_IT_CONTROL );
        ADS_CONTROLS.put( PagedResults.OID, ControlEnum.PAGED_RESULTS_CONTROL );
        ADS_CONTROLS.put( PasswordPolicy.OID, ControlEnum.PASSWORD_POLICY_REQUEST_CONTROL );
        ADS_CONTROLS.put( PersistentSearch.OID, ControlEnum.PERSISTENT_SEARCH_CONTROL );
        ADS_CONTROLS.put( Subentries.OID, ControlEnum.SUBENTRIES_CONTROL );
        ADS_CONTROLS.put( SyncDoneValue.OID, ControlEnum.SYNC_DONE_VALUE_CONTROL );
        ADS_CONTROLS.put( SyncInfoValue.OID, ControlEnum.SYNC_INFO_VALUE_CONTROL );
        ADS_CONTROLS.put( SyncRequestValue.OID, ControlEnum.SYNC_REQUEST_VALUE_CONTROL );
        ADS_CONTROLS.put( SyncStateValue.OID, ControlEnum.SYNC_STATE_VALUE_CONTROL );
    }


    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------
    /**
     * Must be called by all subclasses to initialize the nexus proxy and the
     * environment settings to be used by this Context implementation.  This
     * specific constructor relies on the presence of the {@link
     * Context#PROVIDER_URL} key and value to determine the distinguished name
     * of the newly created context.  It also checks to make sure the
     * referenced name actually exists within the system.  This constructor
     * is used for all InitialContext requests.
     *
     * @param service the parent service that manages this context
     * @param env the environment properties used by this context.
     * @throws NamingException if the environment parameters are not set
     * correctly.
     */
    protected ServerContext( DirectoryService service, Hashtable<String, Object> env ) throws Exception
    {
        this.service = service;
        this.env = env;

        LdapJndiProperties props = LdapJndiProperties.getLdapJndiProperties( this.env );
        dn = props.getProviderDn();

        /*
         * Need do bind operation here, and bindContext returned contains the
         * newly created session.
         */
        BindOperationContext bindContext = doBindOperation( props.getBindDn(), props.getCredentials(),
            props.getSaslMechanism(), props.getSaslAuthId() );

        session = bindContext.getSession();
        OperationManager operationManager = service.getOperationManager();

        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( session, dn );

        if ( !operationManager.hasEntry( hasEntryContext ) )
        {
            throw new NameNotFoundException( I18n.err( I18n.ERR_490, dn ) );
        }

        schemaManager = service.getSchemaManager();

        // setup attribute type value
        OBJECT_CLASS_AT = schemaManager.getAttributeType( SchemaConstants.OBJECT_CLASS_AT );
    }


    /**
     * Must be called by all subclasses to initialize the nexus proxy and the
     * environment settings to be used by this Context implementation.  This
     * constructor is used to propagate new contexts from existing contexts.
     *
     * @param service the directory service core
     * @param principal the directory user principal that is propagated
     * @param name the distinguished name of this context
     * @throws NamingException if there is a problem creating the new context
     */
    public ServerContext( DirectoryService service, LdapPrincipal principal, Name name ) throws Exception
    {
        this.service = service;
        this.dn = JndiUtils.fromName( name );

        this.env = new Hashtable<String, Object>();
        this.env.put( PROVIDER_URL, dn.toString() );
        this.env.put( DirectoryService.JNDI_KEY, service );
        session = new DefaultCoreSession( principal, service );
        OperationManager operationManager = service.getOperationManager();

        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( session, dn );

        if ( !operationManager.hasEntry( hasEntryContext ) )
        {
            throw new NameNotFoundException( I18n.err( I18n.ERR_490, dn ) );
        }

        schemaManager = service.getSchemaManager();

        // setup attribute type value
        OBJECT_CLASS_AT = schemaManager.getAttributeType( SchemaConstants.OBJECT_CLASS_AT );
    }


    public ServerContext( DirectoryService service, CoreSession session, Name name ) throws Exception
    {
        this.service = service;
        this.dn = JndiUtils.fromName( name );
        this.env = new Hashtable<String, Object>();
        this.env.put( PROVIDER_URL, dn.toString() );
        this.env.put( DirectoryService.JNDI_KEY, service );
        this.session = session;
        OperationManager operationManager = service.getOperationManager();

        HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( session, dn );

        if ( !operationManager.hasEntry( hasEntryContext ) )
        {
            throw new NameNotFoundException( I18n.err( I18n.ERR_490, dn ) );
        }

        schemaManager = service.getSchemaManager();

        // setup attribute type value
        OBJECT_CLASS_AT = schemaManager.getAttributeType( SchemaConstants.OBJECT_CLASS_AT );
    }


    /**
     * Set the referral handling flag into the operation context using
     * the JNDI value stored into the environment.
     */
    protected void injectReferralControl( OperationContext opCtx )
    {
        if ( "ignore".equalsIgnoreCase( ( String ) env.get( Context.REFERRAL ) ) )
        {
            opCtx.ignoreReferral();
        }
        else if ( "throw".equalsIgnoreCase( ( String ) env.get( Context.REFERRAL ) ) )
        {
            opCtx.throwReferral();
        }
        else
        {
            // TODO : handle the 'follow' referral option
            opCtx.throwReferral();
        }
    }


    // ------------------------------------------------------------------------
    // Protected Methods for Operations
    // ------------------------------------------------------------------------
    // Use these methods instead of manually calling the nexusProxy so we can
    // add request controls to operation contexts before the call and extract
    // response controls from the contexts after the call.  NOTE that the
    // convertControls( requestControls ) must be cleared after each operation.  This makes a
    // context not thread safe.
    // ------------------------------------------------------------------------

    /**
     * Used to encapsulate [de]marshalling of controls before and after add operations.
     * @param entry
     * @param target
     */
    protected void doAddOperation( Dn target, Entry entry ) throws Exception
    {
        // setup the op context and populate with request controls
        AddOperationContext opCtx = new AddOperationContext( session, entry );

        opCtx.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( opCtx );

        // execute add operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.add( opCtx );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            opCtx.getResponseControls() );
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after delete operations.
     * @param target
     */
    protected void doDeleteOperation( Dn target ) throws Exception
    {
        // setup the op context and populate with request controls
        DeleteOperationContext deleteContext = new DeleteOperationContext( session, target );

        deleteContext.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( deleteContext );

        // execute delete operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.delete( deleteContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            deleteContext.getResponseControls() );
    }


    private org.apache.directory.api.ldap.model.message.Control convertControl( boolean isRequest,
        Control jndiControl ) throws DecoderException
    {
        String controlIDStr = jndiControl.getID();
        CodecControl<? extends org.apache.directory.api.ldap.model.message.Control> control = null;

        ControlEnum controlId = ADS_CONTROLS.get( controlIDStr );

        switch ( controlId )
        {
            case CASCADE_CONTROL:
                control = new CascadeDecorator( getDirectoryService().getLdapCodecService(), new CascadeImpl() );

                break;

            case ENTRY_CHANGE_CONTROL:
                control = new EntryChangeDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case MANAGE_DSA_IT_CONTROL:
                control = new ManageDsaITDecorator( getDirectoryService().getLdapCodecService(), new ManageDsaITImpl() );

                break;

            case PAGED_RESULTS_CONTROL:
                control = new PagedResultsDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case PASSWORD_POLICY_REQUEST_CONTROL:
                if ( isRequest )
                {
                    control = new PasswordPolicyDecorator( getDirectoryService().getLdapCodecService(),
                        new PasswordPolicyImpl() );
                }
                else
                {
                    control = new PasswordPolicyDecorator( getDirectoryService().getLdapCodecService(),
                        new PasswordPolicyImpl( new PasswordPolicyResponseImpl() ) );
                }

                break;

            case PERSISTENT_SEARCH_CONTROL:
                control = new PersistentSearchDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case SUBENTRIES_CONTROL:
                control = new SubentriesDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case SYNC_DONE_VALUE_CONTROL:
                control = new SyncDoneValueDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case SYNC_INFO_VALUE_CONTROL:
                control = new SyncInfoValueDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case SYNC_REQUEST_VALUE_CONTROL:
                control = new SyncRequestValueDecorator( getDirectoryService().getLdapCodecService() );

                break;

            case SYNC_STATE_VALUE_CONTROL:
                control = new SyncStateValueDecorator( getDirectoryService().getLdapCodecService() );

                break;
        }

        control.setCritical( jndiControl.isCritical() );
        control.setValue( jndiControl.getEncodedValue() );

        byte[] value = jndiControl.getEncodedValue();

        if ( !Strings.isEmpty( value ) )
        {
            control.decode( value );
        }

        return control;
    }


    /**
     * Convert the JNDI controls to ADS controls
     * TODO convertControls.
     */
    private org.apache.directory.api.ldap.model.message.Control[] convertControls( boolean isRequest,
        Control[] jndiControls ) throws DecoderException
    {
        if ( jndiControls != null )
        {
            org.apache.directory.api.ldap.model.message.Control[] controls =
                new org.apache.directory.api.ldap.model.message.Control[jndiControls.length];
            int i = 0;

            for ( javax.naming.ldap.Control jndiControl : jndiControls )
            {
                controls[i++] = convertControl( isRequest, jndiControl );
            }

            return controls;
        }
        else
        {
            return null;
        }

    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after list operations.
     * @param dn
     * @param aliasDerefMode
     * @param filter
     * @param searchControls
     * @return NamingEnumeration
     */
    protected EntryFilteringCursor doSearchOperation( Dn dn, AliasDerefMode aliasDerefMode,
        ExprNode filter, SearchControls searchControls ) throws Exception
    {
        OperationManager operationManager = service.getOperationManager();
        EntryFilteringCursor results = null;

        Object typesOnlyObj = getEnvironment().get( "java.naming.ldap.typesOnly" );
        boolean typesOnly = false;

        if ( typesOnlyObj != null )
        {
            typesOnly = Boolean.parseBoolean( typesOnlyObj.toString() );
        }

        SearchOperationContext searchContext = null;

        // We have to check if it's a compare operation or a search.
        // A compare operation has a OBJECT scope search, the filter must
        // be of the form (object=value) (no wildcards), and no attributes
        // should be asked to be returned.
        if ( ( searchControls.getSearchScope() == SearchControls.OBJECT_SCOPE )
            && ( ( searchControls.getReturningAttributes() != null )
            && ( searchControls.getReturningAttributes().length == 0 ) )
            && ( filter instanceof EqualityNode ) )
        {
            CompareOperationContext compareContext = new CompareOperationContext( session, dn,
                ( ( EqualityNode<?> ) filter ).getAttribute(), ( ( EqualityNode ) filter ).getValue() );

            // Inject the referral handling into the operation context
            injectReferralControl( compareContext );

            // Call the operation
            boolean result = operationManager.compare( compareContext );

            // setup the op context and populate with request controls
            searchContext = new SearchOperationContext( session, dn, filter,
                searchControls );
            searchContext.setAliasDerefMode( aliasDerefMode );
            searchContext.addRequestControls( convertControls( true, requestControls ) );

            searchContext.setTypesOnly( typesOnly );

            if ( result )
            {
                Entry emptyEntry = new DefaultEntry( service.getSchemaManager(), Dn.EMPTY_DN );
                return new BaseEntryFilteringCursor( new SingletonCursor<Entry>( emptyEntry ), 
                    searchContext, schemaManager );
            }
            else
            {
                return new BaseEntryFilteringCursor( new EmptyCursor<Entry>(), searchContext, schemaManager );
            }
        }
        else
        {
            // It's a Search

            // setup the op context and populate with request controls
            searchContext = new SearchOperationContext( session, dn, filter, searchControls );
            searchContext.setAliasDerefMode( aliasDerefMode );
            searchContext.addRequestControls( convertControls( true, requestControls ) );
            searchContext.setTypesOnly( typesOnly );

            // Inject the referral handling into the operation context
            injectReferralControl( searchContext );

            // execute search operation
            results = operationManager.search( searchContext );
        }

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            searchContext.getResponseControls() );

        return results;
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after list operations.
     */
    protected EntryFilteringCursor doListOperation( Dn target ) throws Exception
    {
        // setup the op context and populate with request controls
        PresenceNode filter = new PresenceNode( OBJECT_CLASS_AT );
        SearchOperationContext searchContext = new SearchOperationContext( session, target, SearchScope.ONELEVEL, filter, SchemaConstants.ALL_USER_ATTRIBUTES_ARRAY );
        searchContext.addRequestControls( convertControls( true, requestControls ) );

        // execute search operation
        OperationManager operationManager = service.getOperationManager();
        EntryFilteringCursor results = operationManager.search( searchContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            searchContext.getResponseControls() );

        return results;
    }


    protected Entry doGetRootDseOperation( Dn target ) throws Exception
    {
        GetRootDseOperationContext getRootDseContext = new GetRootDseOperationContext( session, target );
        getRootDseContext.addRequestControls( convertControls( true, requestControls ) );

        // do not reset request controls since this is not an external
        // operation and not do bother setting the response controls either
        OperationManager operationManager = service.getOperationManager();

        return operationManager.getRootDse( getRootDseContext );
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after lookup operations.
     */
    protected Entry doLookupOperation( Dn target ) throws Exception
    {
        // setup the op context and populate with request controls
        // execute lookup/getRootDSE operation
        LookupOperationContext lookupContext = new LookupOperationContext( session, target );
        lookupContext.addRequestControls( convertControls( true, requestControls ) );
        OperationManager operationManager = service.getOperationManager();
        Entry serverEntry = operationManager.lookup( lookupContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            lookupContext.getResponseControls() );
        return serverEntry;
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after lookup operations.
     */
    protected Entry doLookupOperation( Dn target, String[] attrIds ) throws Exception
    {
        // setup the op context and populate with request controls
        // execute lookup/getRootDSE operation
        LookupOperationContext lookupContext = new LookupOperationContext( session, target, attrIds );
        lookupContext.addRequestControls( convertControls( true, requestControls ) );
        OperationManager operationManager = service.getOperationManager();
        Entry serverEntry = operationManager.lookup( lookupContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls(
            getDirectoryService().getLdapCodecService(),
            lookupContext.getResponseControls() );

        // Now remove the ObjectClass attribute if it has not been requested
        if ( ( lookupContext.getReturningAttributes() != null ) && ( lookupContext.getReturningAttributes().size() != 0 ) &&
            ( ( serverEntry.get( SchemaConstants.OBJECT_CLASS_AT ) != null )
            && ( serverEntry.get( SchemaConstants.OBJECT_CLASS_AT ).size() == 0 ) ) )
        {
            serverEntry.removeAttributes( SchemaConstants.OBJECT_CLASS_AT );
        }

        return serverEntry;
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after bind operations.
     */
    protected BindOperationContext doBindOperation( Dn bindDn, byte[] credentials, String saslMechanism,
        String saslAuthId ) throws Exception
    {
        // setup the op context and populate with request controls
        BindOperationContext bindContext = new BindOperationContext( null );
        bindContext.setDn( bindDn );
        bindContext.setCredentials( credentials );
        bindContext.setSaslMechanism( saslMechanism );
        bindContext.setSaslAuthId( saslAuthId );
        bindContext.addRequestControls( convertControls( true, requestControls ) );
        bindContext.setInterceptors( getDirectoryService().getInterceptors( OperationEnum.BIND ) );

        // execute bind operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.bind( bindContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            bindContext.getResponseControls() );
        return bindContext;
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after moveAndRename operations.
     */
    protected void doMoveAndRenameOperation( Dn oldDn, Dn parent, Rdn newRdn, boolean delOldDn )
        throws Exception
    {
        // setup the op context and populate with request controls
        MoveAndRenameOperationContext moveAndRenameContext = new MoveAndRenameOperationContext( session, oldDn, parent,
            new Rdn(
                newRdn ), delOldDn );
        moveAndRenameContext.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( moveAndRenameContext );

        // execute moveAndRename operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.moveAndRename( moveAndRenameContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            moveAndRenameContext.getResponseControls() );
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after modify operations.
     */
    protected void doModifyOperation( Dn dn, List<Modification> modifications ) throws Exception
    {
        // setup the op context and populate with request controls
        ModifyOperationContext modifyContext = new ModifyOperationContext( session, dn, modifications );
        modifyContext.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( modifyContext );

        // execute modify operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.modify( modifyContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            modifyContext.getResponseControls() );
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after moveAndRename operations.
     */
    protected void doMove( Dn oldDn, Dn target ) throws Exception
    {
        // setup the op context and populate with request controls
        MoveOperationContext moveContext = new MoveOperationContext( session, oldDn, target );
        moveContext.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( moveContext );

        // execute move operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.move( moveContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            moveContext.getResponseControls() );
    }


    /**
     * Used to encapsulate [de]marshalling of controls before and after rename operations.
     */
    protected void doRename( Dn oldDn, Rdn newRdn, boolean delOldRdn ) throws Exception
    {
        // setup the op context and populate with request controls
        RenameOperationContext renameContext = new RenameOperationContext( session, oldDn, newRdn, delOldRdn );
        renameContext.addRequestControls( convertControls( true, requestControls ) );

        // Inject the referral handling into the operation context
        injectReferralControl( renameContext );

        // execute rename operation
        OperationManager operationManager = service.getOperationManager();
        operationManager.rename( renameContext );

        // clear the request controls and set the response controls
        requestControls = EMPTY_CONTROLS;
        responseControls = JndiUtils.toJndiControls( getDirectoryService().getLdapCodecService(),
            renameContext.getResponseControls() );
    }


    public CoreSession getSession()
    {
        return session;
    }


    public DirectoryService getDirectoryService()
    {
        return service;
    }


    // ------------------------------------------------------------------------
    // New Impl Specific Public Methods
    // ------------------------------------------------------------------------

    /**
     * Gets a handle on the root context of the DIT.  The RootDSE as the present user.
     *
     * @return the rootDSE context
     * @throws NamingException if this fails
     */
    public abstract ServerContext getRootContext() throws NamingException;


    /**
     * Gets the {@link DirectoryService} associated with this context.
     *
     * @return the directory service associated with this context
     */
    public DirectoryService getService()
    {
        return service;
    }


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

    /**
     * Gets the distinguished name of the entry associated with this Context.
     *
     * @return the distinguished name of this Context's entry.
     */
    protected Dn getDn()
    {
        return dn;
    }


    // ------------------------------------------------------------------------
    // JNDI Context Interface Methods
    // ------------------------------------------------------------------------

    /**
     * @see javax.naming.Context#close()
     */
    public void close() throws NamingException
    {
        for ( DirectoryListener listener : listeners.values() )
        {
            try
            {
                service.getEventService().removeListener( listener );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
        }

        listeners.clear();
    }


    /**
     * @see javax.naming.Context#getNameInNamespace()
     */
    public String getNameInNamespace() throws NamingException
    {
        return dn.getName();
    }


    /**
     * @see javax.naming.Context#getEnvironment()
     */
    public Hashtable<String, Object> getEnvironment()
    {
        return env;
    }


    /**
     * @see javax.naming.Context#addToEnvironment(java.lang.String,
     * java.lang.Object)
     */
    public Object addToEnvironment( String propName, Object propVal ) throws NamingException
    {
        return env.put( propName, propVal );
    }


    /**
     * @see javax.naming.Context#removeFromEnvironment(java.lang.String)
     */
    public Object removeFromEnvironment( String propName ) throws NamingException
    {
        return env.remove( propName );
    }


    /**
     * @see javax.naming.Context#createSubcontext(java.lang.String)
     */
    public Context createSubcontext( String name ) throws NamingException
    {
        return createSubcontext( new LdapName( name ) );
    }


    /**
     * @see javax.naming.Context#createSubcontext(javax.naming.Name)
     */
    public Context createSubcontext( Name name ) throws NamingException
    {
        Dn target = buildTarget( JndiUtils.fromName( name ) );
        Entry serverEntry = null;

        try
        {
            serverEntry = service.newEntry( target );
        }
        catch ( LdapException le )
        {
            throw new NamingException( le.getMessage() );
        }

        try
        {
            serverEntry.add( SchemaConstants.OBJECT_CLASS_AT, SchemaConstants.TOP_OC, JavaLdapSupport.JCONTAINER_ATTR );
        }
        catch ( LdapException le )
        {
            throw new SchemaViolationException( I18n.err( I18n.ERR_491, name ) );
        }

        // Now add the CN attribute, which is mandatory
        Rdn rdn = target.getRdn();

        if ( rdn != null )
        {
            if ( SchemaConstants.CN_AT_OID.equals( rdn.getNormType() ) )
            {
                serverEntry.put( rdn.getType(), rdn.getValue() );
            }
            else
            {
                // No CN in the rdn, this is an error
                throw new SchemaViolationException( I18n.err( I18n.ERR_491, name ) );
            }
        }
        else
        {
            // No CN in the rdn, this is an error
            throw new SchemaViolationException( I18n.err( I18n.ERR_491, name ) );
        }

        /*
         * Add the new context to the server which as a side effect adds
         * operational attributes to the serverEntry refering instance which
         * can them be used to initialize a new ServerLdapContext.  Remember
         * we need to copy over the controls as well to propagate the complete
         * environment besides what's in the hashtable for env.
         */
        try
        {
            doAddOperation( target, serverEntry );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }

        ServerLdapContext ctx = null;

        try
        {
            ctx = new ServerLdapContext( service, session.getEffectivePrincipal(), JndiUtils.toName( target ) );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }

        return ctx;
    }


    /**
     * @see javax.naming.Context#destroySubcontext(java.lang.String)
     */
    public void destroySubcontext( String name ) throws NamingException
    {
        destroySubcontext( new LdapName( name ) );
    }


    /**
     * @see javax.naming.Context#destroySubcontext(javax.naming.Name)
     */
    public void destroySubcontext( Name name ) throws NamingException
    {
        Dn target = buildTarget( JndiUtils.fromName( name ) );

        if ( target.size() == 0 )
        {
            throw new NoPermissionException( I18n.err( I18n.ERR_492 ) );
        }

        try
        {
            doDeleteOperation( target );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }
    }


    /**
     * @see javax.naming.Context#bind(java.lang.String, java.lang.Object)
     */
    public void bind( String name, Object obj ) throws NamingException
    {
        bind( new LdapName( name ), obj );
    }


    private void injectRdnAttributeValues( Dn target, Entry serverEntry ) throws NamingException
    {
        // Add all the Rdn attributes and their values to this entry
        Rdn rdn = target.getRdn();

        if ( rdn.size() == 1 )
        {
            serverEntry.put( rdn.getType(), rdn.getValue() );
        }
        else
        {
            for ( Ava atav : rdn )
            {
                serverEntry.put( atav.getType(), atav.getValue() );
            }
        }
    }


    /**
     * @see javax.naming.Context#bind(javax.naming.Name, java.lang.Object)
     */
    public void bind( Name name, Object obj ) throws NamingException
    {
        // First, use state factories to do a transformation
        DirStateFactory.Result res = DirectoryManager.getStateToBind( obj, name, this, env, null );

        Dn target = buildTarget( JndiUtils.fromName( name ) );

        // let's be sure that the Attributes is case insensitive
        Entry outServerEntry = null;

        try
        {
            outServerEntry = ServerEntryUtils.toServerEntry( AttributeUtils.toCaseInsensitive( res
                .getAttributes() ), target, service.getSchemaManager() );
        }
        catch ( LdapInvalidAttributeTypeException liate )
        {
            throw new NamingException( I18n.err( I18n.ERR_495, obj ) );
        }

        if ( outServerEntry != null )
        {
            try
            {
                // Remember : a JNDI Bind is a LDAP Add ! Silly, insn't it ?
                doAddOperation( target, outServerEntry );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
            
            return;
        }

        if ( obj instanceof Entry )
        {
            try
            {
                doAddOperation( target, ( Entry ) obj );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
        }
        // Check for Referenceable
        else if ( obj instanceof Referenceable )
        {
            throw new NamingException( I18n.err( I18n.ERR_493 ) );
        }
        // Store different formats
        else if ( obj instanceof Reference )
        {
            // Store as ref and add outAttrs
            throw new NamingException( I18n.err( I18n.ERR_494 ) );
        }
        else if ( obj instanceof Serializable )
        {
            // Serialize and add outAttrs
            Entry serverEntry = null;

            try
            {
                serverEntry = service.newEntry( target );
            }
            catch ( LdapException le )
            {
                throw new NamingException( I18n.err( I18n.ERR_495, obj ) );
            }

            // Get target and inject all rdn attributes into entry
            injectRdnAttributeValues( target, serverEntry );

            // Serialize object into entry attributes and add it.
            try
            {
                JavaLdapSupport.serialize( serverEntry, obj, service.getSchemaManager() );
            }
            catch ( LdapException le )
            {
                throw new NamingException( I18n.err( I18n.ERR_495, obj ) );
            }

            try
            {
                doAddOperation( target, serverEntry );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
        }
        else if ( obj instanceof DirContext )
        {
            // Grab attributes and merge with outAttrs
            Entry serverEntry = null;

            try
            {
                serverEntry = ServerEntryUtils.toServerEntry( ( ( DirContext ) obj ).getAttributes( "" ),
                    target, service.getSchemaManager() );
            }
            catch ( LdapInvalidAttributeTypeException liate )
            {
                throw new NamingException( I18n.err( I18n.ERR_495, obj ) );
            }

            injectRdnAttributeValues( target, serverEntry );
            
            try
            {
                doAddOperation( target, serverEntry );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
        }
        else
        {
            throw new NamingException( I18n.err( I18n.ERR_495, obj ) );
        }
    }


    /**
     * @see javax.naming.Context#rename(java.lang.String, java.lang.String)
     */
    public void rename( String oldName, String newName ) throws NamingException
    {
        rename( new LdapName( oldName ), new LdapName( newName ) );
    }


    /**
     * @see javax.naming.Context#rename(javax.naming.Name, javax.naming.Name)
     */
    public void rename( Name oldName, Name newName ) throws NamingException
    {
        Dn oldDn = buildTarget( JndiUtils.fromName( oldName ) );
        Dn newDn = buildTarget( JndiUtils.fromName( newName ) );

        if ( oldDn.size() == 0 )
        {
            throw new NoPermissionException( I18n.err( I18n.ERR_312 ) );
        }

        // calculate parents
        Dn oldParent = oldDn;

        oldParent = oldParent.getParent();

        Dn newParent = newDn;

        newParent = newParent.getParent();

        Rdn oldRdn = oldDn.getRdn();
        Rdn newRdn = newDn.getRdn();
        boolean delOldRdn = true;

        /*
         * Attempt to use the java.naming.ldap.deleteRDN environment property
         * to get an override for the deleteOldRdn option to modifyRdn.
         */
        if ( null != env.get( DELETE_OLD_RDN_PROP ) )
        {
            String delOldRdnStr = ( String ) env.get( DELETE_OLD_RDN_PROP );
            delOldRdn = !delOldRdnStr.equalsIgnoreCase( "false" ) && !delOldRdnStr.equalsIgnoreCase( "no" )
                && !delOldRdnStr.equals( "0" );
        }

        /*
         * We need to determine if this rename operation corresponds to a simple
         * Rdn name change or a move operation.  If the two names are the same
         * except for the Rdn then it is a simple modifyRdn operation.  If the
         * names differ in size or have a different baseDN then the operation is
         * a move operation.  Furthermore if the Rdn in the move operation
         * changes it is both an Rdn change and a move operation.
         */
        if ( oldParent.equals( newParent ) )
        {
            try
            {
                doRename( oldDn, newRdn, delOldRdn );
            }
            catch ( Exception e )
            {
                JndiUtils.wrap( e );
            }
        }
        else
        {
            if ( newRdn.equals( oldRdn ) )
            {
                try
                {
                    doMove( oldDn, newParent );
                }
                catch ( Exception e )
                {
                    JndiUtils.wrap( e );
                }
            }
            else
            {
                try
                {
                    doMoveAndRenameOperation( oldDn, newParent, newRdn, delOldRdn );
                }
                catch ( Exception e )
                {
                    JndiUtils.wrap( e );
                }
            }
        }
    }


    /**
     * @see javax.naming.Context#rebind(java.lang.String, java.lang.Object)
     */
    public void rebind( String name, Object obj ) throws NamingException
    {
        rebind( new LdapName( name ), obj );
    }


    /**
     * @see javax.naming.Context#rebind(javax.naming.Name, java.lang.Object)
     */
    public void rebind( Name name, Object obj ) throws NamingException
    {
        Dn target = buildTarget( JndiUtils.fromName( name ) );
        OperationManager operationManager = service.getOperationManager();

        try
        {
            HasEntryOperationContext hasEntryContext = new HasEntryOperationContext( session, target );

            if ( operationManager.hasEntry( hasEntryContext ) )
            {
                doDeleteOperation( target );
            }
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }

        bind( name, obj );
    }


    /**
     * @see javax.naming.Context#unbind(java.lang.String)
     */
    public void unbind( String name ) throws NamingException
    {
        unbind( new LdapName( name ) );
    }


    /**
     * @see javax.naming.Context#unbind(javax.naming.Name)
     */
    public void unbind( Name name ) throws NamingException
    {
        try
        {
            doDeleteOperation( buildTarget( JndiUtils.fromName( name ) ) );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }
    }


    /**
     * @see javax.naming.Context#lookup(java.lang.String)
     */
    public Object lookup( String name ) throws NamingException
    {
        if ( Strings.isEmpty( name ) )
        {
            return lookup( new LdapName( "" ) );
        }
        else
        {
            return lookup( new LdapName( name ) );
        }
    }


    /**
     * @see javax.naming.Context#lookup(javax.naming.Name)
     */
    public Object lookup( Name name ) throws NamingException
    {
        Object obj;
        Dn target = buildTarget( JndiUtils.fromName( name ) );

        Entry serverEntry = null;

        try
        {
            if ( name.size() == 0 )
            {
                serverEntry = doGetRootDseOperation( target );
            }
            else
            {
                serverEntry = doLookupOperation( target );
            }
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }

        try
        {
            obj = DirectoryManager.getObjectInstance( null, name, this, env,
                ServerEntryUtils.toBasicAttributes( serverEntry ) );
        }
        catch ( Exception e )
        {
            String msg = I18n.err( I18n.ERR_497, target );
            NamingException ne = new NamingException( msg );
            ne.setRootCause( e );
            throw ne;
        }

        if ( obj != null )
        {
            return obj;
        }

        // First lets test and see if the entry is a serialized java object
        if ( serverEntry.get( JavaLdapSupport.JCLASSNAME_ATTR ) != null )
        {
            // Give back serialized object and not a context
            return JavaLdapSupport.deserialize( serverEntry );
        }

        // Initialize and return a context since the entry is not a java object
        ServerLdapContext ctx = null;

        try
        {
            ctx = new ServerLdapContext( service, session.getEffectivePrincipal(), JndiUtils.toName( target ) );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }

        return ctx;
    }


    /**
     * @see javax.naming.Context#lookupLink(java.lang.String)
     */
    public Object lookupLink( String name ) throws NamingException
    {
        throw new UnsupportedOperationException();
    }


    /**
     * @see javax.naming.Context#lookupLink(javax.naming.Name)
     */
    public Object lookupLink( Name name ) throws NamingException
    {
        throw new UnsupportedOperationException();
    }


    /**
     * Non-federated implementation presuming the name argument is not a
     * composite name spanning multiple namespaces but a compound name in
     * the same LDAP namespace.  Hence the parser returned is always the
     * same as calling this method with the empty String.
     *
     * @see javax.naming.Context#getNameParser(java.lang.String)
     */
    public NameParser getNameParser( String name ) throws NamingException
    {
        return new NameParser()
        {
            public Name parse( String name ) throws NamingException
            {
                try
                {
                    return JndiUtils.toName( new Dn( name ) );
                }
                catch ( LdapInvalidDnException lide )
                {
                    throw new InvalidNameException( lide.getMessage() );
                }
            }
        };
    }


    /**
     * Non-federated implementation presuming the name argument is not a
     * composite name spanning multiple namespaces but a compound name in
     * the same LDAP namespace.  Hence the parser returned is always the
     * same as calling this method with the empty String Name.
     *
     * @see javax.naming.Context#getNameParser(javax.naming.Name)
     */
    public NameParser getNameParser( final Name name ) throws NamingException
    {
        return new NameParser()
        {
            public Name parse( String n ) throws NamingException
            {
                try
                {
                    return JndiUtils.toName( new Dn( name.toString() ) );
                }
                catch ( LdapInvalidDnException lide )
                {
                    throw new InvalidNameException( lide.getMessage() );
                }
            }
        };
    }


    /**
     * @see javax.naming.Context#list(java.lang.String)
     */
    @SuppressWarnings(value =
        { "unchecked" })
    public NamingEnumeration list( String name ) throws NamingException
    {
        return list( new LdapName( name ) );
    }


    /**
     * @see javax.naming.Context#list(javax.naming.Name)
     */
    @SuppressWarnings(value =
        { "unchecked" })
    public NamingEnumeration list( Name name ) throws NamingException
    {
        try
        {
            return new NamingEnumerationAdapter( doListOperation( buildTarget( JndiUtils.fromName( name ) ) ) );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
            return null; // shut up compiler
        }
    }


    /**
     * @see javax.naming.Context#listBindings(java.lang.String)
     */
    @SuppressWarnings(value =
        { "unchecked" })
    public NamingEnumeration listBindings( String name ) throws NamingException
    {
        return listBindings( new LdapName( name ) );
    }


    /**
     * @see javax.naming.Context#listBindings(javax.naming.Name)
     */
    @SuppressWarnings(value =
        { "unchecked" })
    public NamingEnumeration listBindings( Name name ) throws NamingException
    {
        // Conduct a special one level search at base for all objects
        Dn base = buildTarget( JndiUtils.fromName( name ) );
        PresenceNode filter = new PresenceNode( OBJECT_CLASS_AT );
        SearchControls ctls = new SearchControls();
        ctls.setSearchScope( SearchControls.ONELEVEL_SCOPE );
        AliasDerefMode aliasDerefMode = AliasDerefMode.getEnum( getEnvironment() );
        try
        {
            return new NamingEnumerationAdapter( doSearchOperation( base, aliasDerefMode, filter, ctls ) );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
            return null; // shutup compiler
        }
    }


    /**
     * @see javax.naming.Context#composeName(java.lang.String, java.lang.String)
     */
    public String composeName( String name, String prefix ) throws NamingException
    {
        return composeName( new LdapName( name ), new LdapName( prefix ) ).toString();
    }


    /**
     * @see javax.naming.Context#composeName(javax.naming.Name,
     * javax.naming.Name)
     */
    public Name composeName( Name name, Name prefix ) throws NamingException
    {
        // No prefix reduces to name, or the name relative to this context
        if ( ( prefix == null ) || ( prefix.size() == 0 ) )
        {
            return name;
        }

        /*
         * Example: This context is ou=people and say name is the relative
         * name of uid=jwalker and the prefix is dc=domain.  Then we must
         * compose the name relative to prefix which would be:
         *
         * uid=jwalker,ou=people,dc=domain.
         *
         * The following general algorithm generates the right name:
         *      1). Find the Dn for name and walk it from the head to tail
         *          trying to match for the head of prefix.
         *      2). Remove name components from the Dn until a match for the
         *          head of the prefix is found.
         *      3). Return the remainder of the fqn or Dn after chewing off some
         */

        // 1). Find the Dn for name and walk it from the head to tail
        Dn fqn = buildTarget( JndiUtils.fromName( name ) );

        try
        {
            return JndiUtils.toName( JndiUtils.fromName( prefix ).add( fqn ) );
        }
        catch ( LdapInvalidDnException lide )
        {
            throw new InvalidNameException( lide.getMessage() );
        }
    }


    // ------------------------------------------------------------------------
    // EventContext implementations
    // ------------------------------------------------------------------------

    public void addNamingListener( Name name, int scope, NamingListener namingListener ) throws NamingException
    {
        ExprNode filter = new PresenceNode( OBJECT_CLASS_AT );

        try
        {
            DirectoryListener listener = new EventListenerAdapter( ( ServerLdapContext ) this, namingListener );
            NotificationCriteria criteria = new NotificationCriteria();
            criteria.setFilter( filter );
            criteria.setScope( SearchScope.getSearchScope( scope ) );
            criteria.setAliasDerefMode( AliasDerefMode.getEnum( env ) );
            criteria.setBase( buildTarget( JndiUtils.fromName( name ) ) );

            service.getEventService().addListener( listener, criteria );
            listeners.put( namingListener, listener );
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }
    }


    public void addNamingListener( String name, int scope, NamingListener namingListener ) throws NamingException
    {
        addNamingListener( new LdapName( name ), scope, namingListener );
    }


    public void removeNamingListener( NamingListener namingListener ) throws NamingException
    {
        try
        {
            DirectoryListener listener = listeners.remove( namingListener );

            if ( listener != null )
            {
                service.getEventService().removeListener( listener );
            }
        }
        catch ( Exception e )
        {
            JndiUtils.wrap( e );
        }
    }


    public boolean targetMustExist() throws NamingException
    {
        return false;
    }


    /**
     * Allows subclasses to register and unregister listeners.
     *
     * @return the set of listeners used for tracking registered name listeners.
     */
    protected Map<NamingListener, DirectoryListener> getListeners()
    {
        return listeners;
    }


    // ------------------------------------------------------------------------
    // Utility Methods to Reduce Code
    // ------------------------------------------------------------------------

    /**
     * Clones this context's Dn and adds the components of the name relative to
     * this context to the left hand side of this context's cloned Dn.
     *
     * @param relativeName a name relative to this context.
     * @return the name of the target
     * @throws InvalidNameException if relativeName is not a valid name in
     *      the LDAP namespace.
     */
    Dn buildTarget( Dn relativeName ) throws NamingException
    {
        Dn target = dn;

        // Add to left hand side of cloned Dn the relative name arg
        try
        {
            target = target.add( relativeName );
            target.apply( schemaManager );
        }
        catch ( LdapInvalidDnException lide )
        {
            throw new InvalidNameException( lide.getMessage() );
        }

        return target;
    }
}
