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.api.filtering;
021
022
023import java.io.IOException;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.List;
027
028import org.apache.directory.api.ldap.model.constants.Loggers;
029import org.apache.directory.api.ldap.model.cursor.AbstractCursor;
030import org.apache.directory.api.ldap.model.cursor.ClosureMonitor;
031import org.apache.directory.api.ldap.model.cursor.Cursor;
032import org.apache.directory.api.ldap.model.cursor.CursorException;
033import org.apache.directory.api.ldap.model.cursor.InvalidCursorPositionException;
034import org.apache.directory.api.ldap.model.entry.Entry;
035import org.apache.directory.api.ldap.model.exception.LdapException;
036import org.apache.directory.api.ldap.model.exception.OperationAbandonedException;
037import org.apache.directory.api.ldap.model.schema.SchemaManager;
038import org.apache.directory.server.core.api.entry.ClonedServerEntry;
039import org.apache.directory.server.core.api.entry.ServerEntryUtils;
040import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044
045/**
046 * A Cursor which uses a list of filters to selectively return entries and/or
047 * modify the contents of entries.  Uses lazy pre-fetching on positioning
048 * operations which means adding filters after creation will not miss candidate
049 * entries.
050 *
051 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
052 */
053public class EntryFilteringCursorImpl extends AbstractCursor<Entry> implements EntryFilteringCursor
054{
055    /** the logger used by this class */
056    private static final Logger LOG = LoggerFactory.getLogger( EntryFilteringCursorImpl.class );
057
058    /** A dedicated log for cursors */
059    private static final Logger LOG_CURSOR = LoggerFactory.getLogger( Loggers.CURSOR_LOG.getName() );
060
061    /** Speedup for logs */
062    private static final boolean IS_DEBUG = LOG_CURSOR.isDebugEnabled();
063
064    /** the underlying wrapped search results Cursor */
065    private final Cursor<Entry> wrapped;
066
067    /** the parameters associated with the search operation */
068    private final SearchOperationContext operationContext;
069
070    /** The SchemaManager */
071    private final SchemaManager schemaManager;
072
073    /** the list of filters to be applied */
074    private final List<EntryFilter> filters;
075
076    /** the first accepted search result that is pre fetched */
077    private Entry prefetched;
078
079
080    // ------------------------------------------------------------------------
081    // C O N S T R U C T O R S
082    // ------------------------------------------------------------------------
083
084    /**
085     * Creates a new entry filtering Cursor over an existing Cursor using a
086     * single filter initially: more can be added later after creation.
087     * 
088     * @param wrapped the underlying wrapped Cursor whose entries are filtered
089     * @param schemaManager The SchemaManager instance
090     * @param operationContext The OperationContext instance
091     * @param filter a single filter to be used
092     */
093    public EntryFilteringCursorImpl( Cursor<Entry> wrapped,
094        SearchOperationContext operationContext, SchemaManager schemaManager, EntryFilter filter )
095    {
096        this( wrapped, operationContext, schemaManager, Collections.singletonList( filter ) );
097    }
098
099
100    /**
101     * Creates a new entry filtering Cursor over an existing Cursor using a
102     * no filter initially: more can be added later after creation.
103     * 
104     * @param wrapped the underlying wrapped Cursor whose entries are filtered
105     * @param operationContext The OperationContext instance
106     * @param schemaManager The SchemaManager instance
107     */
108    public EntryFilteringCursorImpl( Cursor<Entry> wrapped, SearchOperationContext operationContext,
109        SchemaManager schemaManager )
110    {
111        if ( IS_DEBUG )
112        {
113            LOG_CURSOR.debug( "Creating BaseEntryFilteringCursor {}", this );
114        }
115
116        this.wrapped = wrapped;
117        this.operationContext = operationContext;
118        this.filters = new ArrayList<>();
119        this.schemaManager = schemaManager;
120    }
121
122
123    /**
124     * Creates a new entry filtering Cursor over an existing Cursor using a
125     * list of filters initially: more can be added later after creation.
126     * 
127     * @param wrapped the underlying wrapped Cursor whose entries are filtered
128     * @param operationContext the operation context that created this Cursor
129     * @param schemaManager The SchemaManager instance
130     * @param filters a list of filters to be used
131     */
132    public EntryFilteringCursorImpl( Cursor<Entry> wrapped,
133        SearchOperationContext operationContext,
134        SchemaManager schemaManager,
135        List<EntryFilter> filters )
136    {
137        if ( IS_DEBUG )
138        {
139            LOG_CURSOR.debug( "Creating BaseEntryFilteringCursor {}", this );
140        }
141
142        this.wrapped = wrapped;
143        this.operationContext = operationContext;
144        this.filters = new ArrayList<>();
145        this.filters.addAll( filters );
146        this.schemaManager = schemaManager;
147    }
148
149
150    // ------------------------------------------------------------------------
151    // Class Specific Methods
152    // ------------------------------------------------------------------------
153
154    /* (non-Javadoc)
155     * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#isAbandoned()
156     */
157    public boolean isAbandoned()
158    {
159        return operationContext.isAbandoned();
160    }
161
162
163    /* (non-Javadoc)
164     * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#setAbandoned(boolean)
165     */
166    public void setAbandoned( boolean abandoned )
167    {
168        operationContext.setAbandoned( abandoned );
169
170        if ( abandoned )
171        {
172            LOG.info( "Cursor has been abandoned." );
173        }
174    }
175
176
177    /* (non-Javadoc)
178     * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#addEntryFilter(org.apache.directory.server.core.filtering.EntryFilter)
179     */
180    public boolean addEntryFilter( EntryFilter filter )
181    {
182        return filters.add( filter );
183    }
184
185
186    /* (non-Javadoc)
187     * @see org.apache.directory.server.core.filtering.EntryFilteringCursor#removeEntryFilter(org.apache.directory.server.core.filtering.EntryFilter)
188     */
189    public boolean removeEntryFilter( EntryFilter filter )
190    {
191        return filters.remove( filter );
192    }
193
194
195    /**
196     * {@inheritDoc}
197     */
198    public List<EntryFilter> getEntryFilters()
199    {
200        return Collections.unmodifiableList( filters );
201    }
202
203
204    /**
205     * {@inheritDoc}
206     */
207    public SearchOperationContext getOperationContext()
208    {
209        return operationContext;
210    }
211
212
213    // ------------------------------------------------------------------------
214    // Cursor Interface Methods
215    // ------------------------------------------------------------------------
216    /**
217     * {@inheritDoc}
218     */
219    public void after( Entry element ) throws LdapException, CursorException
220    {
221        throw new UnsupportedOperationException();
222    }
223
224
225    /**
226     * {@inheritDoc}
227     */
228    public void afterLast() throws LdapException, CursorException
229    {
230        wrapped.afterLast();
231        prefetched = null;
232    }
233
234
235    /**
236     * {@inheritDoc}
237     */
238    public boolean available()
239    {
240        return prefetched != null;
241    }
242
243
244    /**
245     * {@inheritDoc}
246     */
247    public void before( Entry element ) throws LdapException, CursorException
248    {
249        throw new UnsupportedOperationException();
250    }
251
252
253    /**
254     * {@inheritDoc}
255     */
256    public void beforeFirst() throws LdapException, CursorException
257    {
258        wrapped.beforeFirst();
259        prefetched = null;
260    }
261
262
263    /**
264     * {@inheritDoc}
265     */
266    public void close() throws IOException
267    {
268        if ( IS_DEBUG )
269        {
270            LOG_CURSOR.debug( "Closing BaseEntryFilteringCursor {}", this );
271        }
272
273        wrapped.close();
274        prefetched = null;
275    }
276
277
278    /**
279     * {@inheritDoc}
280     */
281    public void close( Exception reason ) throws IOException
282    {
283        if ( IS_DEBUG )
284        {
285            LOG_CURSOR.debug( "Closing BaseEntryFilteringCursor {}", this );
286        }
287
288        wrapped.close( reason );
289        prefetched = null;
290    }
291
292
293    /**
294     * {@inheritDoc}
295     */
296    public final void setClosureMonitor( ClosureMonitor monitor )
297    {
298        wrapped.setClosureMonitor( monitor );
299    }
300
301
302    /**
303     * {@inheritDoc}
304     */
305    public boolean first() throws LdapException, CursorException
306    {
307        if ( operationContext.isAbandoned() )
308        {
309            LOG.info( "Cursor has been abandoned." );
310            
311            try
312            {
313                close();
314            }
315            catch ( IOException ioe )
316            {
317                throw new LdapException( ioe.getMessage(), ioe );
318            }
319
320            throw new OperationAbandonedException();
321        }
322
323        beforeFirst();
324
325        return next();
326    }
327
328
329    /**
330     * {@inheritDoc}
331     */
332    public Entry get() throws InvalidCursorPositionException
333    {
334        if ( available() )
335        {
336            return prefetched;
337        }
338
339        throw new InvalidCursorPositionException();
340    }
341
342
343    /**
344     * {@inheritDoc}
345     */
346    public boolean isClosed()
347    {
348        return wrapped.isClosed();
349    }
350
351
352    /**
353     * {@inheritDoc}
354     */
355    public boolean last() throws LdapException, CursorException
356    {
357        if ( operationContext.isAbandoned() )
358        {
359            LOG.info( "Cursor has been abandoned." );
360            
361            try
362            {
363                close();
364            }
365            catch ( IOException ioe )
366            {
367                throw new LdapException( ioe.getMessage(), ioe );
368            }
369
370            throw new OperationAbandonedException();
371        }
372
373        afterLast();
374
375        return previous();
376    }
377
378
379    /**
380     * {@inheritDoc}
381     */
382    public boolean next() throws LdapException, CursorException
383    {
384        if ( operationContext.isAbandoned() )
385        {
386            LOG.info( "Cursor has been abandoned." );
387            
388            try
389            {
390                close();
391            }
392            catch ( IOException ioe )
393            {
394                throw new LdapException( ioe.getMessage(), ioe );
395            }
396
397            throw new OperationAbandonedException();
398        }
399
400        Entry tempResult = null;
401
402        outer: while ( wrapped.next() )
403        {
404            Entry tempEntry = wrapped.get();
405
406            if ( tempEntry == null )
407            {
408                // no candidate
409                continue;
410            }
411
412            if ( tempEntry instanceof ClonedServerEntry )
413            {
414                tempResult = tempEntry;
415            }
416            else
417            {
418                tempResult = new ClonedServerEntry( tempEntry );
419            }
420
421            /*
422             * O P T I M I Z A T I O N
423             * -----------------------
424             * 
425             * Don't want to waste cycles on enabling a loop for processing
426             * filters if we have zero or one filter.
427             */
428
429            if ( filters.isEmpty() )
430            {
431                prefetched = tempResult;
432                ServerEntryUtils.filterContents(
433                    schemaManager,
434                    operationContext, prefetched );
435
436                return true;
437            }
438
439            if ( ( filters.size() == 1 ) && filters.get( 0 ).accept( operationContext, tempResult ) )
440            {
441                prefetched = tempResult;
442                ServerEntryUtils.filterContents(
443                    schemaManager,
444                    operationContext, prefetched );
445
446                return true;
447            }
448
449            /* E N D   O P T I M I Z A T I O N */
450            for ( EntryFilter filter : filters )
451            {
452                // if a filter rejects then short and continue with outer loop
453                if ( !filter.accept( operationContext, tempResult ) )
454                {
455                    continue outer;
456                }
457            }
458
459            /*
460             * Here the entry has been accepted by all filters.
461             */
462            prefetched = tempResult;
463
464            return true;
465        }
466
467        prefetched = null;
468
469        return false;
470    }
471
472
473    /**
474     * {@inheritDoc}
475     */
476    public boolean previous() throws LdapException, CursorException
477    {
478        if ( operationContext.isAbandoned() )
479        {
480            LOG.info( "Cursor has been abandoned." );
481            
482            try
483            {
484                close();
485            }
486            catch ( IOException ioe )
487            {
488                throw new LdapException( ioe.getMessage(), ioe );
489            }
490
491            throw new OperationAbandonedException();
492        }
493
494        Entry tempResult = null;
495
496        outer: while ( wrapped.previous() )
497        {
498            Entry entry = wrapped.get();
499
500            if ( entry == null )
501            {
502                continue;
503            }
504
505            tempResult = new ClonedServerEntry/*Search*/( entry );
506
507            /*
508             * O P T I M I Z A T I O N
509             * -----------------------
510             * 
511             * Don't want to waste cycles on enabling a loop for processing
512             * filters if we have zero or one filter.
513             */
514
515            if ( filters.isEmpty() )
516            {
517                prefetched = tempResult;
518                ServerEntryUtils.filterContents(
519                    schemaManager,
520                    operationContext, prefetched );
521
522                return true;
523            }
524
525            if ( ( filters.size() == 1 ) && filters.get( 0 ).accept( operationContext, tempResult ) )
526            {
527                prefetched = tempResult;
528                ServerEntryUtils.filterContents(
529                    schemaManager,
530                    operationContext, prefetched );
531
532                return true;
533            }
534
535            /* E N D   O P T I M I Z A T I O N */
536
537            for ( EntryFilter filter : filters )
538            {
539                // if a filter rejects then short and continue with outer loop
540                if ( !filter.accept( operationContext, tempResult ) )
541                {
542                    continue outer;
543                }
544            }
545
546            /*
547             * Here the entry has been accepted by all filters.
548             */
549            prefetched = tempResult;
550            ServerEntryUtils.filterContents(
551                schemaManager,
552                operationContext, prefetched );
553
554            return true;
555        }
556
557        prefetched = null;
558
559        return false;
560    }
561
562
563    /**
564     * @see Object#toString()
565     */
566    public String toString( String tabs )
567    {
568        StringBuilder sb = new StringBuilder();
569
570        if ( wrapped != null )
571        {
572            sb.append( tabs ).append( "BaseEntryFilteringCursor, wrapped : \n" );
573            sb.append( wrapped.toString( tabs + "    " ) );
574        }
575        else
576        {
577            sb.append( tabs ).append( "BaseEntryFilteringCursor, no wrapped\n" );
578        }
579
580        if ( ( filters != null ) && !filters.isEmpty() )
581        {
582            sb.append( tabs ).append( "Filters : \n" );
583
584            for ( EntryFilter filter : filters )
585            {
586                sb.append( filter.toString( tabs + "    " ) ).append( "\n" );
587            }
588        }
589        else
590        {
591            sb.append( tabs ).append( "No filter\n" );
592        }
593
594        if ( prefetched != null )
595        {
596            sb.append( tabs ).append( "Prefetched : \n" );
597            sb.append( prefetched.toString( tabs + "    " ) );
598        }
599        else
600        {
601            sb.append( tabs ).append( "No prefetched" );
602        }
603
604        return sb.toString();
605    }
606
607
608    /**
609     * @see Object#toString()
610     */
611    public String toString()
612    {
613        return toString( "" );
614    }
615}