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


import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.AbandonListener;
import org.apache.directory.api.ldap.model.message.AbandonableRequest;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchResultEntry;
import org.apache.directory.api.ldap.model.message.SearchResultEntryImpl;
import org.apache.directory.api.ldap.model.message.controls.ChangeType;
import org.apache.directory.api.ldap.model.message.controls.EntryChange;
import org.apache.directory.api.ldap.model.message.controls.EntryChangeImpl;
import org.apache.directory.api.ldap.model.message.controls.PersistentSearch;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.api.entry.ClonedServerEntry;
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.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ChangeOperationContext;
import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
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.RenameOperationContext;
import org.apache.directory.server.ldap.LdapSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A DirectoryListener implementation which sends back added, deleted, modified or 
 * renamed entries to a client that created this listener.  This class is part of the
 * persistent search implementation which uses the event notification scheme built into
 * the server core.  
 * 
 * This listener is disabled only when a session closes or when an abandon request 
 * cancels it.  Hence time and size limits in normal search operations do not apply
 * here.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class PersistentSearchListener implements DirectoryListener, AbandonListener
{
    private static final Logger LOG = LoggerFactory.getLogger( PersistentSearchListener.class );
    final LdapSession session;
    final SearchRequest req;
    final PersistentSearch psearchControl;

    private LookupOperationContext filterCtx;
    private SchemaManager schemaManager;

    public PersistentSearchListener( LdapSession session, SearchRequest req )
    {
        this.session = session;
        this.req = req;
        req.addAbandonListener( this );
        this.psearchControl = ( PersistentSearch ) req.getControls().get( PersistentSearch.OID );
        
        filterCtx = new LookupOperationContext( session.getCoreSession(), req.getAttributes().toArray( Strings.EMPTY_STRING_ARRAY ) );
        schemaManager = session.getCoreSession().getDirectoryService().getSchemaManager();
    }

    
    @Override
    public boolean isSynchronous()
    {
        return false; // always asynchronous
    }


    public void abandon()
    {
        // must abandon the operation 
        session.getCoreSession().getDirectoryService().getEventService().removeListener( this );

        /*
         * From RFC 2251 Section 4.11:
         * 
         * In the event that a server receives an Abandon Request on a Search  
         * operation in the midst of transmitting responses to the Search, that
         * server MUST cease transmitting entry responses to the abandoned
         * request immediately, and MUST NOT send the SearchResultDone. Of
         * course, the server MUST ensure that only properly encoded LDAPMessage
         * PDUs are transmitted. 
         * 
         * SO DON'T SEND BACK ANYTHING!!!!!
         */
    }


    public void requestAbandoned( AbandonableRequest req )
    {
        abandon();
    }


    private void setECResponseControl( SearchResultEntry response, ChangeOperationContext opContext, ChangeType type )
    {
        if ( psearchControl.isReturnECs() )
        {
            EntryChange ecControl = new EntryChangeImpl();
            ecControl.setChangeType( type );

            if ( opContext.getChangeLogEvent() != null )
            {
                ecControl.setChangeNumber( opContext.getChangeLogEvent().getRevision() );
            }

            if ( opContext instanceof RenameOperationContext || opContext instanceof MoveOperationContext )
            {
                ecControl.setPreviousDn( opContext.getDn() );
            }

            response.addControl( ecControl );
        }
    }


    public void entryAdded( AddOperationContext addContext )
    {
        if ( !psearchControl.isNotificationEnabled( ChangeType.ADD ) )
        {
            return;
        }

        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
        respEntry.setObjectName( addContext.getDn() );
        
        // the entry needs to be cloned cause addContext.getEntry() will only contain
        // the user provided values and all the operational attributes added during
        // Partition.add() will be applied in the cloned entry present inside it
        // if we don't clone then the attributes will not be filtered
        // e.x the operational attributes will also be sent even when a user requests
        // user attributes only
        Entry entry = new ClonedServerEntry( addContext.getEntry() );
        filterEntry( entry );
        respEntry.setEntry( entry );
        
        setECResponseControl( respEntry, addContext, ChangeType.ADD );
        session.getIoSession().write( respEntry );
    }


    public void entryDeleted( DeleteOperationContext deleteContext )
    {
        if ( !psearchControl.isNotificationEnabled( ChangeType.DELETE ) )
        {
            return;
        }

        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
        respEntry.setObjectName( deleteContext.getDn() );
        filterEntry( deleteContext.getEntry() );
        respEntry.setEntry( deleteContext.getEntry() );
        setECResponseControl( respEntry, deleteContext, ChangeType.DELETE );
        session.getIoSession().write( respEntry );
    }


    public void entryModified( ModifyOperationContext modifyContext )
    {
        if ( !psearchControl.isNotificationEnabled( ChangeType.MODIFY ) )
        {
            return;
        }

        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
        respEntry.setObjectName( modifyContext.getDn() );
        
        Entry entry = new ClonedServerEntry( modifyContext.getAlteredEntry() );
        filterEntry( entry );
        respEntry.setEntry( entry );

        setECResponseControl( respEntry, modifyContext, ChangeType.MODIFY );
        session.getIoSession().write( respEntry );
    }


    public void entryMoved( MoveOperationContext moveContext )
    {
        if ( !psearchControl.isNotificationEnabled( ChangeType.MODDN ) )
        {
            return;
        }

        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
        respEntry.setObjectName( moveContext.getNewDn() );
        
        Entry entry = new ClonedServerEntry( moveContext.getModifiedEntry() );
        filterEntry( entry );
        respEntry.setEntry( entry );
        
        setECResponseControl( respEntry, moveContext, ChangeType.MODDN );
        session.getIoSession().write( respEntry );
    }


    public void entryMovedAndRenamed( MoveAndRenameOperationContext moveAndRenameContext )
    {
        entryRenamed( moveAndRenameContext );
    }


    public void entryRenamed( RenameOperationContext renameContext )
    {
        if ( !psearchControl.isNotificationEnabled( ChangeType.MODDN ) )
        {
            return;
        }

        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
        respEntry.setObjectName( renameContext.getModifiedEntry().getDn() );
        
        Entry entry = new ClonedServerEntry( renameContext.getModifiedEntry() );
        filterEntry( entry );
        respEntry.setEntry( entry );
        
        setECResponseControl( respEntry, renameContext, ChangeType.MODDN );
        session.getIoSession().write( respEntry );
    }
    
    
    /**
     * A convenient method to filter the contents of an entry
     * 
     * @see ServerEntryUtils#filterContents(SchemaManager, org.apache.directory.server.core.api.interceptor.context.FilteringOperationContext, Entry)
     * 
     * @param entry
     */
    private void filterEntry( Entry entry )
    {
        try
        {
            ServerEntryUtils.filterContents( schemaManager, filterCtx, entry );
        }
        catch ( LdapException e )
        {
            // shouldn't happen, if it does then blow up
            throw new RuntimeException( e );
        }
    }
}