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.core.event;
021
022
023import static org.apache.directory.api.ldap.model.message.SearchScope.OBJECT;
024import static org.apache.directory.api.ldap.model.message.SearchScope.ONELEVEL;
025import static org.apache.directory.api.ldap.model.message.SearchScope.SUBTREE;
026
027import java.util.ArrayList;
028import java.util.Collections;
029import java.util.List;
030import java.util.concurrent.ArrayBlockingQueue;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Executors;
033import java.util.concurrent.ThreadFactory;
034import java.util.concurrent.ThreadPoolExecutor;
035import java.util.concurrent.TimeUnit;
036
037import org.apache.directory.api.ldap.model.constants.SchemaConstants;
038import org.apache.directory.api.ldap.model.entry.Entry;
039import org.apache.directory.api.ldap.model.exception.LdapException;
040import org.apache.directory.api.ldap.model.message.SearchScope;
041import org.apache.directory.api.ldap.model.name.Dn;
042import org.apache.directory.server.core.api.CoreSession;
043import org.apache.directory.server.core.api.DirectoryService;
044import org.apache.directory.server.core.api.InterceptorEnum;
045import org.apache.directory.server.core.api.entry.ClonedServerEntry;
046import org.apache.directory.server.core.api.event.DirectoryListener;
047import org.apache.directory.server.core.api.event.Evaluator;
048import org.apache.directory.server.core.api.event.EventType;
049import org.apache.directory.server.core.api.event.ExpressionEvaluator;
050import org.apache.directory.server.core.api.event.NotificationCriteria;
051import org.apache.directory.server.core.api.event.RegistrationEntry;
052import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
053import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
054import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
055import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
056import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
057import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
058import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
059import org.apache.directory.server.core.api.interceptor.context.OperationContext;
060import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064/**
065 * An {@link org.apache.directory.server.core.api.interceptor.Interceptor} based service for notifying {@link
066 * DirectoryListener}s of changes to the DIT.
067 *
068 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
069 */
070public class EventInterceptor extends BaseInterceptor
071{
072    /** A logger for this class */
073    private static final Logger LOG = LoggerFactory.getLogger( EventInterceptor.class );
074
075    private Evaluator evaluator;
076    private ExecutorService executor;
077
078
079    /**
080     * Creates a new instance of a EventInterceptor.
081     */
082    public EventInterceptor()
083    {
084        super( InterceptorEnum.EVENT_INTERCEPTOR );
085    }
086
087
088    /**
089     * Initialize the event interceptor. It creates a pool of executor which will be used
090     * to call the listeners in separate threads.
091     */
092    @Override
093    public void init( DirectoryService directoryService ) throws LdapException
094    {
095        LOG.info( "Initializing ..." );
096        super.init( directoryService );
097
098        evaluator = new ExpressionEvaluator( schemaManager );
099        executor = new ThreadPoolExecutor( 1, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>( 100 ) );
100        
101        ThreadFactory threadFactory = new ThreadFactory() 
102        {
103            @Override
104            public Thread newThread( Runnable runnable ) 
105            {
106                Thread newThread = Executors.defaultThreadFactory().newThread( runnable );
107                newThread.setDaemon( true );
108                
109                return newThread;
110            }
111        };
112        
113        executor = new ThreadPoolExecutor( 1, 10, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>( 100 ),
114            threadFactory );
115
116        this.directoryService.setEventService( new DefaultEventService( directoryService ) );
117        LOG.info( "Initialization complete." );
118    }
119
120
121    /**
122     * Call the listener passing it the context.
123     */
124    private void fire( final OperationContext opContext, EventType type, final DirectoryListener listener )
125    {
126        switch ( type )
127        {
128            case ADD:
129                if ( listener.isSynchronous() )
130                {
131                    listener.entryAdded( ( AddOperationContext ) opContext );
132                }
133                else
134                {
135                    executor.execute( new Runnable()
136                    {
137                        @Override
138                        public void run()
139                        {
140                            listener.entryAdded( ( AddOperationContext ) opContext );
141                        }
142                    } );
143                }
144
145                break;
146
147            case DELETE:
148                if ( listener.isSynchronous() )
149                {
150                    listener.entryDeleted( ( DeleteOperationContext ) opContext );
151                }
152                else
153                {
154                    executor.execute( new Runnable()
155                    {
156                        @Override
157                        public void run()
158                        {
159                            listener.entryDeleted( ( DeleteOperationContext ) opContext );
160                        }
161                    } );
162                }
163
164                break;
165
166            case MODIFY:
167                if ( listener.isSynchronous() )
168                {
169                    listener.entryModified( ( ModifyOperationContext ) opContext );
170                }
171                else
172                {
173                    executor.execute( new Runnable()
174                    {
175                        @Override
176                        public void run()
177                        {
178                            listener.entryModified( ( ModifyOperationContext ) opContext );
179                        }
180                    } );
181                }
182
183                break;
184
185            case MOVE:
186                if ( listener.isSynchronous() )
187                {
188                    listener.entryMoved( ( MoveOperationContext ) opContext );
189                }
190                else
191                {
192                    executor.execute( new Runnable()
193                    {
194                        @Override
195                        public void run()
196                        {
197                            listener.entryMoved( ( MoveOperationContext ) opContext );
198                        }
199                    } );
200                }
201
202                break;
203
204            case RENAME:
205                if ( listener.isSynchronous() )
206                {
207                    listener.entryRenamed( ( RenameOperationContext ) opContext );
208                }
209                else
210                {
211                    executor.execute( new Runnable()
212                    {
213                        @Override
214                        public void run()
215                        {
216                            listener.entryRenamed( ( RenameOperationContext ) opContext );
217                        }
218                    } );
219                }
220
221                break;
222
223            case MOVE_AND_RENAME:
224                if ( listener.isSynchronous() )
225                {
226                    listener.entryMovedAndRenamed( ( MoveAndRenameOperationContext ) opContext );
227                }
228                else
229                {
230                    executor.execute( new Runnable()
231                    {
232                        @Override
233                        public void run()
234                        {
235                            listener.entryMovedAndRenamed( ( MoveAndRenameOperationContext ) opContext );
236                        }
237                    } );
238                }
239
240                break;
241
242            default:
243                throw new IllegalArgumentException( "Unexpected event type " + type );
244        }
245    }
246
247
248    /**
249     * {@inheritDoc}
250     */
251    @Override
252    public void add( final AddOperationContext addContext ) throws LdapException
253    {
254        next( addContext );
255
256        List<RegistrationEntry> selecting = getSelectingRegistrations( addContext.getDn(), addContext.getEntry() );
257
258        if ( selecting.isEmpty() )
259        {
260            return;
261        }
262
263        for ( final RegistrationEntry registration : selecting )
264        {
265            if ( EventType.isAdd( registration.getCriteria().getEventMask() ) )
266            {
267                fire( addContext, EventType.ADD, registration.getListener() );
268            }
269        }
270    }
271
272
273    /**
274     * {@inheritDoc}
275     */
276    @Override
277    public void delete( final DeleteOperationContext deleteContext ) throws LdapException
278    {
279        next( deleteContext );
280
281        List<RegistrationEntry> selecting = getSelectingRegistrations( deleteContext.getDn(), deleteContext.getEntry() );
282
283        if ( selecting.isEmpty() )
284        {
285            return;
286        }
287
288        for ( final RegistrationEntry registration : selecting )
289        {
290            if ( EventType.isDelete( registration.getCriteria().getEventMask() ) )
291            {
292                fire( deleteContext, EventType.DELETE, registration.getListener() );
293            }
294        }
295    }
296
297
298    /**
299     * {@inheritDoc}
300     */
301    @Override
302    public void modify( final ModifyOperationContext modifyContext ) throws LdapException
303    {
304        Entry oriEntry = modifyContext.getEntry();
305
306        // modification was already done when this flag is turned on, move to sending the events
307        if ( !modifyContext.isPushToEvtInterceptor() )
308        {
309            next( modifyContext );
310        }
311
312        List<RegistrationEntry> selecting = getSelectingRegistrations( modifyContext.getDn(), oriEntry );
313
314        if ( selecting.isEmpty() )
315        {
316            return;
317        }
318
319        // Get the modified entry
320        CoreSession session = modifyContext.getSession();
321        LookupOperationContext lookupContext = new LookupOperationContext( session, modifyContext.getDn(),
322            SchemaConstants.ALL_ATTRIBUTES_ARRAY );
323        lookupContext.setPartition( modifyContext.getPartition() );
324        lookupContext.setTransaction( modifyContext.getTransaction() );
325
326        Entry alteredEntry = directoryService.getPartitionNexus().lookup( lookupContext );
327        modifyContext.setAlteredEntry( alteredEntry );
328
329        for ( final RegistrationEntry registration : selecting )
330        {
331            if ( EventType.isModify( registration.getCriteria().getEventMask() ) )
332            {
333                fire( modifyContext, EventType.MODIFY, registration.getListener() );
334            }
335        }
336    }
337
338
339    /**
340     * {@inheritDoc}
341     */
342    @Override
343    public void move( MoveOperationContext moveContext ) throws LdapException
344    {
345        Entry oriEntry = moveContext.getOriginalEntry();
346
347        next( moveContext );
348
349        List<RegistrationEntry> selecting = getSelectingRegistrations( moveContext.getDn(), oriEntry );
350
351        if ( selecting.isEmpty() )
352        {
353            return;
354        }
355
356        for ( final RegistrationEntry registration : selecting )
357        {
358            if ( EventType.isMove( registration.getCriteria().getEventMask() ) )
359            {
360                fire( moveContext, EventType.MOVE, registration.getListener() );
361            }
362        }
363    }
364
365
366    /**
367     * {@inheritDoc}
368     */
369    @Override
370    public void moveAndRename( final MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
371    {
372        Entry oriEntry = moveAndRenameContext.getOriginalEntry();
373        next( moveAndRenameContext );
374
375        List<RegistrationEntry> selecting = getSelectingRegistrations( moveAndRenameContext.getDn(), oriEntry );
376
377        if ( selecting.isEmpty() )
378        {
379            return;
380        }
381
382        for ( final RegistrationEntry registration : selecting )
383        {
384            if ( EventType.isMoveAndRename( registration.getCriteria().getEventMask() ) )
385            {
386                fire( moveAndRenameContext, EventType.MOVE_AND_RENAME, registration.getListener() );
387            }
388        }
389    }
390
391
392    /**
393     * {@inheritDoc}
394     */
395    @Override
396    public void rename( RenameOperationContext renameContext ) throws LdapException
397    {
398        Entry oriEntry = ( ( ClonedServerEntry ) renameContext.getEntry() ).getOriginalEntry();
399
400        next( renameContext );
401
402        List<RegistrationEntry> selecting = getSelectingRegistrations( renameContext.getDn(), oriEntry );
403
404        if ( selecting.isEmpty() )
405        {
406            return;
407        }
408
409        // Get the modified entry
410        CoreSession session = renameContext.getSession();
411        LookupOperationContext lookupContext = new LookupOperationContext( session, renameContext.getNewDn(),
412            SchemaConstants.ALL_ATTRIBUTES_ARRAY );
413        lookupContext.setPartition( renameContext.getPartition() );
414        lookupContext.setTransaction( renameContext.getTransaction() );
415
416        Entry alteredEntry = directoryService.getPartitionNexus().lookup( lookupContext );
417        renameContext.setModifiedEntry( alteredEntry );
418
419        for ( final RegistrationEntry registration : selecting )
420        {
421            if ( EventType.isRename( registration.getCriteria().getEventMask() ) )
422            {
423                fire( renameContext, EventType.RENAME, registration.getListener() );
424            }
425        }
426    }
427
428
429    /**
430     * Find a list of registrationEntries given an entry and a name. We check against
431     * the criteria for each registrationEntry
432     */
433    private List<RegistrationEntry> getSelectingRegistrations( Dn name, Entry entry ) throws LdapException
434    {
435        List<RegistrationEntry> registrations = directoryService.getEventService().getRegistrationEntries();
436
437        if ( registrations.isEmpty() )
438        {
439            return Collections.emptyList();
440        }
441
442        List<RegistrationEntry> selecting = new ArrayList<>();
443
444        for ( RegistrationEntry registration : registrations )
445        {
446            NotificationCriteria criteria = registration.getCriteria();
447
448            Dn base = criteria.getBase();
449
450            SearchScope scope = criteria.getScope();
451            
452            // fix for DIRSERVER-1502
453            boolean inscope =
454                    ( ( ( scope == OBJECT ) && name.equals( base ) )
455                    || ( ( scope == ONELEVEL ) && name.getParent().equals( base ) )
456                    || ( ( scope == SUBTREE ) && ( name.isDescendantOf( base ) || name.equals( base ) ) ) );
457            
458            if ( inscope && evaluator.evaluate( criteria.getFilter(), base, entry ) )
459            {
460                selecting.add( registration );
461            }
462        }
463
464        return selecting;
465    }
466
467    
468    /**
469     * {@inheritDoc}
470     */
471    @Override
472    public void destroy()
473    {
474       executor.shutdown();
475    }
476}