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