001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *  
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *  
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License. 
018 *  
019 */
020package org.apache.directory.server.ldap.handlers;
021
022
023import org.apache.directory.api.ldap.model.entry.Entry;
024import org.apache.directory.api.ldap.model.exception.LdapException;
025import org.apache.directory.api.ldap.model.message.AbandonListener;
026import org.apache.directory.api.ldap.model.message.AbandonableRequest;
027import org.apache.directory.api.ldap.model.message.SearchRequest;
028import org.apache.directory.api.ldap.model.message.SearchResultEntry;
029import org.apache.directory.api.ldap.model.message.SearchResultEntryImpl;
030import org.apache.directory.api.ldap.model.message.controls.ChangeType;
031import org.apache.directory.api.ldap.model.message.controls.EntryChange;
032import org.apache.directory.api.ldap.model.message.controls.EntryChangeImpl;
033import org.apache.directory.api.ldap.model.message.controls.PersistentSearch;
034import org.apache.directory.api.ldap.model.schema.SchemaManager;
035import org.apache.directory.api.util.Strings;
036import org.apache.directory.server.core.api.entry.ClonedServerEntry;
037import org.apache.directory.server.core.api.entry.ServerEntryUtils;
038import org.apache.directory.server.core.api.event.DirectoryListener;
039import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
040import org.apache.directory.server.core.api.interceptor.context.ChangeOperationContext;
041import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
042import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
043import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
044import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
045import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
046import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
047import org.apache.directory.server.ldap.LdapSession;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051
052/**
053 * A DirectoryListener implementation which sends back added, deleted, modified or 
054 * renamed entries to a client that created this listener.  This class is part of the
055 * persistent search implementation which uses the event notification scheme built into
056 * the server core.  
057 * 
058 * This listener is disabled only when a session closes or when an abandon request 
059 * cancels it.  Hence time and size limits in normal search operations do not apply
060 * here.
061 * 
062 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
063 */
064public class PersistentSearchListener implements DirectoryListener, AbandonListener
065{
066    private static final Logger LOG = LoggerFactory.getLogger( PersistentSearchListener.class );
067    final LdapSession session;
068    final SearchRequest req;
069    final PersistentSearch psearchControl;
070
071    private LookupOperationContext filterCtx;
072    private SchemaManager schemaManager;
073
074    public PersistentSearchListener( LdapSession session, SearchRequest req )
075    {
076        this.session = session;
077        this.req = req;
078        req.addAbandonListener( this );
079        this.psearchControl = ( PersistentSearch ) req.getControls().get( PersistentSearch.OID );
080        
081        filterCtx = new LookupOperationContext( session.getCoreSession(), req.getAttributes().toArray( Strings.EMPTY_STRING_ARRAY ) );
082        schemaManager = session.getCoreSession().getDirectoryService().getSchemaManager();
083    }
084
085    
086    @Override
087    public boolean isSynchronous()
088    {
089        return false; // always asynchronous
090    }
091
092
093    public void abandon()
094    {
095        // must abandon the operation 
096        session.getCoreSession().getDirectoryService().getEventService().removeListener( this );
097
098        /*
099         * From RFC 2251 Section 4.11:
100         * 
101         * In the event that a server receives an Abandon Request on a Search  
102         * operation in the midst of transmitting responses to the Search, that
103         * server MUST cease transmitting entry responses to the abandoned
104         * request immediately, and MUST NOT send the SearchResultDone. Of
105         * course, the server MUST ensure that only properly encoded LDAPMessage
106         * PDUs are transmitted. 
107         * 
108         * SO DON'T SEND BACK ANYTHING!!!!!
109         */
110    }
111
112
113    public void requestAbandoned( AbandonableRequest req )
114    {
115        abandon();
116    }
117
118
119    private void setECResponseControl( SearchResultEntry response, ChangeOperationContext opContext, ChangeType type )
120    {
121        if ( psearchControl.isReturnECs() )
122        {
123            EntryChange ecControl = new EntryChangeImpl();
124            ecControl.setChangeType( type );
125
126            if ( opContext.getChangeLogEvent() != null )
127            {
128                ecControl.setChangeNumber( opContext.getChangeLogEvent().getRevision() );
129            }
130
131            if ( opContext instanceof RenameOperationContext || opContext instanceof MoveOperationContext )
132            {
133                ecControl.setPreviousDn( opContext.getDn() );
134            }
135
136            response.addControl( ecControl );
137        }
138    }
139
140
141    public void entryAdded( AddOperationContext addContext )
142    {
143        if ( !psearchControl.isNotificationEnabled( ChangeType.ADD ) )
144        {
145            return;
146        }
147
148        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
149        respEntry.setObjectName( addContext.getDn() );
150        
151        // the entry needs to be cloned cause addContext.getEntry() will only contain
152        // the user provided values and all the operational attributes added during
153        // Partition.add() will be applied in the cloned entry present inside it
154        // if we don't clone then the attributes will not be filtered
155        // e.x the operational attributes will also be sent even when a user requests
156        // user attributes only
157        Entry entry = new ClonedServerEntry( addContext.getEntry() );
158        filterEntry( entry );
159        respEntry.setEntry( entry );
160        
161        setECResponseControl( respEntry, addContext, ChangeType.ADD );
162        session.getIoSession().write( respEntry );
163    }
164
165
166    public void entryDeleted( DeleteOperationContext deleteContext )
167    {
168        if ( !psearchControl.isNotificationEnabled( ChangeType.DELETE ) )
169        {
170            return;
171        }
172
173        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
174        respEntry.setObjectName( deleteContext.getDn() );
175        filterEntry( deleteContext.getEntry() );
176        respEntry.setEntry( deleteContext.getEntry() );
177        setECResponseControl( respEntry, deleteContext, ChangeType.DELETE );
178        session.getIoSession().write( respEntry );
179    }
180
181
182    public void entryModified( ModifyOperationContext modifyContext )
183    {
184        if ( !psearchControl.isNotificationEnabled( ChangeType.MODIFY ) )
185        {
186            return;
187        }
188
189        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
190        respEntry.setObjectName( modifyContext.getDn() );
191        
192        Entry entry = new ClonedServerEntry( modifyContext.getAlteredEntry() );
193        filterEntry( entry );
194        respEntry.setEntry( entry );
195
196        setECResponseControl( respEntry, modifyContext, ChangeType.MODIFY );
197        session.getIoSession().write( respEntry );
198    }
199
200
201    public void entryMoved( MoveOperationContext moveContext )
202    {
203        if ( !psearchControl.isNotificationEnabled( ChangeType.MODDN ) )
204        {
205            return;
206        }
207
208        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
209        respEntry.setObjectName( moveContext.getNewDn() );
210        
211        Entry entry = new ClonedServerEntry( moveContext.getModifiedEntry() );
212        filterEntry( entry );
213        respEntry.setEntry( entry );
214        
215        setECResponseControl( respEntry, moveContext, ChangeType.MODDN );
216        session.getIoSession().write( respEntry );
217    }
218
219
220    public void entryMovedAndRenamed( MoveAndRenameOperationContext moveAndRenameContext )
221    {
222        entryRenamed( moveAndRenameContext );
223    }
224
225
226    public void entryRenamed( RenameOperationContext renameContext )
227    {
228        if ( !psearchControl.isNotificationEnabled( ChangeType.MODDN ) )
229        {
230            return;
231        }
232
233        SearchResultEntry respEntry = new SearchResultEntryImpl( req.getMessageId() );
234        respEntry.setObjectName( renameContext.getModifiedEntry().getDn() );
235        
236        Entry entry = new ClonedServerEntry( renameContext.getModifiedEntry() );
237        filterEntry( entry );
238        respEntry.setEntry( entry );
239        
240        setECResponseControl( respEntry, renameContext, ChangeType.MODDN );
241        session.getIoSession().write( respEntry );
242    }
243    
244    
245    /**
246     * A convenient method to filter the contents of an entry
247     * 
248     * @see ServerEntryUtils#filterContents(SchemaManager, org.apache.directory.server.core.api.interceptor.context.FilteringOperationContext, Entry)
249     * 
250     * @param entry
251     */
252    private void filterEntry( Entry entry )
253    {
254        try
255        {
256            ServerEntryUtils.filterContents( schemaManager, filterCtx, entry );
257        }
258        catch ( LdapException e )
259        {
260            // shouldn't happen, if it does then blow up
261            throw new RuntimeException( e );
262        }
263    }
264}