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.ldap.handlers;
021    
022    
023    import java.util.concurrent.TimeUnit;
024    
025    import org.apache.directory.server.core.DirectoryService;
026    import org.apache.directory.server.core.ReferralManager;
027    import org.apache.directory.server.core.entry.ClonedServerEntry;
028    import org.apache.directory.server.core.entry.ServerStringValue;
029    import org.apache.directory.server.core.event.EventType;
030    import org.apache.directory.server.core.event.NotificationCriteria;
031    import org.apache.directory.server.core.filtering.EntryFilteringCursor;
032    import org.apache.directory.server.core.partition.PartitionNexus;
033    import org.apache.directory.server.ldap.LdapSession;
034    import org.apache.directory.server.ldap.handlers.controls.PagedSearchContext;
035    import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
036    import org.apache.directory.shared.ldap.constants.SchemaConstants;
037    import org.apache.directory.shared.ldap.entry.EntryAttribute;
038    import org.apache.directory.shared.ldap.entry.Value;
039    import org.apache.directory.shared.ldap.exception.OperationAbandonedException;
040    import org.apache.directory.shared.ldap.filter.EqualityNode;
041    import org.apache.directory.shared.ldap.filter.OrNode;
042    import org.apache.directory.shared.ldap.filter.PresenceNode;
043    import org.apache.directory.shared.ldap.message.InternalLdapResult;
044    import org.apache.directory.shared.ldap.message.ReferralImpl;
045    import org.apache.directory.shared.ldap.message.InternalResponse;
046    import org.apache.directory.shared.ldap.message.ResultCodeEnum;
047    import org.apache.directory.shared.ldap.filter.SearchScope;
048    import org.apache.directory.shared.ldap.message.InternalReferral;
049    import org.apache.directory.shared.ldap.message.InternalSearchRequest;
050    import org.apache.directory.shared.ldap.message.InternalSearchResponseDone;
051    import org.apache.directory.shared.ldap.message.InternalSearchResponseEntry;
052    import org.apache.directory.shared.ldap.message.SearchResponseEntryImpl;
053    import org.apache.directory.shared.ldap.message.InternalSearchResponseReference;
054    import org.apache.directory.shared.ldap.message.SearchResponseReferenceImpl;
055    import org.apache.directory.shared.ldap.message.control.ManageDsaITControl;
056    import org.apache.directory.shared.ldap.message.control.PagedSearchControl;
057    import org.apache.directory.shared.ldap.message.control.PersistentSearchControl;
058    import org.apache.directory.shared.ldap.name.LdapDN;
059    import org.apache.directory.shared.ldap.schema.AttributeType;
060    import org.apache.directory.shared.ldap.util.LdapURL;
061    import org.apache.directory.shared.ldap.util.StringTools;
062    import org.slf4j.Logger;
063    import org.slf4j.LoggerFactory;
064    
065    import static org.apache.directory.server.ldap.LdapServer.NO_SIZE_LIMIT;
066    import static org.apache.directory.server.ldap.LdapServer.NO_TIME_LIMIT;
067    
068    import javax.naming.NameNotFoundException;
069    import javax.naming.NamingException;
070    import javax.naming.ldap.PagedResultsControl;
071    
072    import static java.lang.Math.min;
073    
074    
075    /**
076     * A handler for processing search requests.
077     *
078     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
079     * @version $Rev: 664302 $
080     */
081    public class SearchHandler extends ReferralAwareRequestHandler<InternalSearchRequest>
082    {
083        private static final Logger LOG = LoggerFactory.getLogger( SearchHandler.class );
084    
085        /** Speedup for logs */
086        private static final boolean IS_DEBUG = LOG.isDebugEnabled();
087    
088        /** cached to save redundant lookups into registries */ 
089        private AttributeType objectClassAttributeType;
090        
091        
092        /**
093         * Constructs a new filter EqualityNode asserting that a candidate 
094         * objectClass is a referral.
095         *
096         * @param session the {@link LdapSession} to construct the node for
097         * @return the {@link EqualityNode} (objectClass=referral) non-normalized
098         * @throws Exception in the highly unlikely event of schema related failures
099         */
100        private EqualityNode<String> newIsReferralEqualityNode( LdapSession session ) throws Exception
101        {
102            if ( objectClassAttributeType == null )
103            {
104                objectClassAttributeType = session.getCoreSession().getDirectoryService().getRegistries()
105                    .getAttributeTypeRegistry().lookup( SchemaConstants.OBJECT_CLASS_AT );
106            }
107            
108            EqualityNode<String> ocIsReferral = new EqualityNode<String>( SchemaConstants.OBJECT_CLASS_AT,
109                new ServerStringValue( objectClassAttributeType, SchemaConstants.REFERRAL_OC ) );
110            
111            return ocIsReferral;
112        }
113        
114        
115        /**
116         * Handles search requests containing the persistent search control but 
117         * delegates to doSimpleSearch() if the changesOnly parameter of the 
118         * control is set to false.
119         *
120         * @param session the LdapSession for which this search is conducted 
121         * @param req the search request containing the persistent search control
122         * @param psearchControl the persistent search control extracted
123         * @throws Exception if failures are encountered while searching
124         */
125        private void handlePersistentSearch( LdapSession session, InternalSearchRequest req, 
126            PersistentSearchControl psearchControl ) throws Exception 
127        {
128            /*
129             * We want the search to complete first before we start listening to 
130             * events when the control does NOT specify changes ONLY mode.
131             */
132            if ( ! psearchControl.isChangesOnly() )
133            {
134                InternalSearchResponseDone done = doSimpleSearch( session, req );
135                
136                // ok if normal search beforehand failed somehow quickly abandon psearch
137                if ( done.getLdapResult().getResultCode() != ResultCodeEnum.SUCCESS )
138                {
139                    session.getIoSession().write( done );
140                    return;
141                }
142            }
143    
144            if ( req.isAbandoned() )
145            {
146                return;
147            }
148            
149            // now we process entries forever as they change
150            PersistentSearchListener handler = new PersistentSearchListener( session, req );
151            
152            // compose notification criteria and add the listener to the event 
153            // service using that notification criteria to determine which events 
154            // are to be delivered to the persistent search issuing client
155            NotificationCriteria criteria = new NotificationCriteria();
156            criteria.setAliasDerefMode( req.getDerefAliases() );
157            criteria.setBase( req.getBase() );
158            criteria.setFilter( req.getFilter() );
159            criteria.setScope( req.getScope() );
160            criteria.setEventMask( EventType.getEventTypes( psearchControl.getChangeTypes() ) );
161            getLdapServer().getDirectoryService().getEventService().addListener( handler, criteria );
162            req.addAbandonListener( new SearchAbandonListener( ldapServer, handler ) );
163            return;
164        }
165        
166        
167        /**
168         * Handles search requests on the RootDSE. 
169         * 
170         * @param session the LdapSession for which this search is conducted 
171         * @param req the search request on the RootDSE
172         * @throws Exception if failures are encountered while searching
173         */
174        private void handleRootDseSearch( LdapSession session, InternalSearchRequest req ) throws Exception
175        {
176            EntryFilteringCursor cursor = null;
177            
178            try
179            {
180                cursor = session.getCoreSession().search( req );
181                
182                // Position the cursor at the beginning
183                cursor.beforeFirst();
184                boolean hasRootDSE = false;
185                
186                while ( cursor.next() )
187                {
188                    if ( hasRootDSE )
189                    {
190                        // This is an error ! We should never find more than one rootDSE !
191                        LOG.error( "Got back more than one entry for search on RootDSE which means " +
192                                "Cursor is not functioning properly!" );
193                    }
194                    else
195                    {
196                        hasRootDSE = true;
197                        ClonedServerEntry entry = cursor.get();
198                        session.getIoSession().write( generateResponse( session, req, entry ) );
199                    }
200                }
201        
202                // write the SearchResultDone message
203                session.getIoSession().write( req.getResultResponse() );
204            }
205            finally
206            {
207                // Close the cursor now.
208                if ( cursor != null )
209                {
210                    try
211                    {
212                        cursor.close();
213                    }
214                    catch ( NamingException e )
215                    {
216                        LOG.error( "failed on list.close()", e );
217                    }
218                }
219            }
220        }
221        
222        
223        /**
224         * Based on the server maximum time limits configured for search and the 
225         * requested time limits this method determines if at all to replace the 
226         * default ClosureMonitor of the result set Cursor with one that closes
227         * the Cursor when either server mandated or request mandated time limits 
228         * are reached.
229         *
230         * @param req the {@link InternalSearchRequest} issued
231         * @param session the {@link LdapSession} on which search was requested
232         * @param cursor the {@link EntryFilteringCursor} over the search results
233         */
234        private void setTimeLimitsOnCursor( InternalSearchRequest req, LdapSession session, final EntryFilteringCursor cursor )
235        {
236            // Don't bother setting time limits for administrators
237            if ( session.getCoreSession().isAnAdministrator() && req.getTimeLimit() == NO_TIME_LIMIT )
238            {
239                return;
240            }
241            
242            /*
243             * Non administrator based searches are limited by time if the server 
244             * has been configured with unlimited time and the request specifies 
245             * unlimited search time
246             */
247            if ( ldapServer.getMaxTimeLimit() == NO_TIME_LIMIT && req.getTimeLimit() == NO_TIME_LIMIT )
248            {
249                return;
250            }
251            
252            /*
253             * If the non-administrator user specifies unlimited time but the server 
254             * is configured to limit the search time then we limit by the max time 
255             * allowed by the configuration 
256             */
257            if ( req.getTimeLimit() == 0 )
258            {
259                cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
260                return;
261            }
262            
263            /*
264             * If the non-administrative user specifies a time limit equal to or 
265             * less than the maximum limit configured in the server then we 
266             * constrain search by the amount specified in the request
267             */
268            if ( ldapServer.getMaxTimeLimit() >= req.getTimeLimit() )
269            {
270                cursor.setClosureMonitor( new SearchTimeLimitingMonitor( req.getTimeLimit(), TimeUnit.SECONDS ) );
271                return;
272            }
273    
274            /*
275             * Here the non-administrative user's requested time limit is greater 
276             * than what the server's configured maximum limit allows so we limit
277             * the search to the configured limit
278             */
279            cursor.setClosureMonitor( new SearchTimeLimitingMonitor( ldapServer.getMaxTimeLimit(), TimeUnit.SECONDS ) );
280        }
281        
282        
283        /**
284         * Return the server size limit
285         */
286        private int getServerSizeLimit( LdapSession session, InternalSearchRequest request )
287        {
288            if ( session.getCoreSession().isAnAdministrator() )
289            {
290                if ( request.getSizeLimit() == NO_SIZE_LIMIT )
291                {
292                    return Integer.MAX_VALUE;
293                }
294                else
295                {
296                    return request.getSizeLimit();
297                }
298            }
299            else
300            {
301                if ( ldapServer.getMaxSizeLimit() == NO_SIZE_LIMIT )
302                {
303                    return Integer.MAX_VALUE;
304                }
305                else
306                {
307                    return ldapServer.getMaxSizeLimit();
308                }
309            }
310        }
311        
312        
313        private void readResults( LdapSession session, InternalSearchRequest req, InternalLdapResult ldapResult,
314        EntryFilteringCursor cursor, int sizeLimit ) throws Exception
315        {
316            int count = 0;
317    
318            while ( (count < sizeLimit ) && cursor.next() )
319            {
320                // Handle closed session
321                if ( session.getIoSession().isClosing() )
322                {
323                    // The client has closed the connection
324                    LOG.debug( "Request terminated for message {}, the client has closed the session", 
325                        req.getMessageId() );
326                    break;
327                }
328    
329                if ( req.isAbandoned() )
330                {
331                    // The cursor has been closed by an abandon request.
332                    LOG.debug( "Request terminated by an AbandonRequest for message {}", 
333                        req.getMessageId() );
334                    break;
335                }
336                
337                ClonedServerEntry entry = cursor.get();
338                session.getIoSession().write( generateResponse( session, req, entry ) );
339                LOG.debug( "Sending {}", entry.getDn() );
340                count++;
341            }
342            
343            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
344            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
345    
346            if ( ( count >= sizeLimit ) && ( cursor.next() ) )
347            {
348                // We have reached the limit
349                // Move backward on the cursor to restore the previous position, as we moved forward
350                // to check if there is one more entry available
351                cursor.previous();
352                // Special case if the user has requested more elements than the request size limit
353                ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
354            }
355        }
356        
357        
358        private void readPagedResults( LdapSession session, InternalSearchRequest req, InternalLdapResult ldapResult,  
359            EntryFilteringCursor cursor, int sizeLimit, int pagedLimit, boolean isPaged, 
360            PagedSearchContext pagedContext, PagedResultsControl pagedResultsControl ) throws Exception
361        {
362            req.addAbandonListener( new SearchAbandonListener( ldapServer, cursor ) );
363            setTimeLimitsOnCursor( req, session, cursor );
364            LOG.debug( "using <{},{}> for size limit", sizeLimit, pagedLimit );
365            int cookieValue = 0;
366            
367            int count = pagedContext.getCurrentPosition();
368            int pageCount = 0;
369            
370            while ( ( count < sizeLimit ) && ( pageCount < pagedLimit ) && cursor.next() )
371            {
372                if ( session.getIoSession().isClosing() )
373                {
374                    break;
375                }
376                
377                ClonedServerEntry entry = cursor.get();
378                session.getIoSession().write( generateResponse( session, req, entry ) );
379                count++;
380                pageCount++;
381            }
382            
383            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
384            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
385            
386            boolean hasMoreEntry = cursor.next();
387            
388            if ( hasMoreEntry )
389            {
390                cursor.previous();
391            }
392    
393            if ( !hasMoreEntry )
394            {
395                // That means we don't have anymore entry
396                // If we are here, it means we have returned all the entries
397                // We have to remove the cookie from the session
398                cookieValue = pagedContext.getCookieValue();
399                PagedSearchContext psCookie = session.removePagedSearchContext( cookieValue );
400                
401                // Close the cursor if there is one
402                if ( psCookie != null )
403                {
404                    cursor = psCookie.getCursor();
405                    
406                    if ( cursor != null )
407                    {
408                        cursor.close();
409                    }
410                }
411                
412                pagedResultsControl = new PagedResultsControl( 0, true );
413                req.getResultResponse().add( pagedResultsControl );
414    
415                return;
416            }
417            else
418            {
419                // We have reached one limit
420                
421                if ( count < sizeLimit )
422                {
423                    // We stop here. We have to add a ResponseControl
424                    // DO NOT WRITE THE RESPONSE - JUST RETURN IT
425                    ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
426                    req.getResultResponse().add( pagedResultsControl );
427                    
428                    // Stores the cursor current position 
429                    pagedContext.incrementCurrentPosition( pageCount );
430                    return;
431                }
432                else
433                {
434                    // Return an exception, close the cursor, and clean the session
435                    ldapResult.setResultCode( ResultCodeEnum.SIZE_LIMIT_EXCEEDED );
436                    
437                    if ( cursor != null )
438                    {
439                        cursor.close();
440                    }
441                    
442                    session.removePagedSearchContext( cookieValue );
443                    
444                    return;
445                }
446            }
447        }
448        
449        
450        /**
451         * Manage the abandoned Paged Search (when paged size = 0). We have to
452         * remove the cookie and its associated cursor from the session.
453         */
454        private InternalSearchResponseDone abandonPagedSearch( LdapSession session, InternalSearchRequest req ) 
455            throws Exception
456        {
457            PagedResultsControl pagedResultsControl = null;
458            PagedSearchControl pagedSearchControl = 
459                ( PagedSearchControl )req.getControls().get( PagedSearchControl.CONTROL_OID );
460            byte [] cookie= pagedSearchControl.getCookie();
461            
462            if ( !StringTools.isEmpty( cookie ) )
463            {
464                // If the cookie is not null, we have to destroy the associated
465                // cursor stored into the session (if any)
466                int cookieValue = pagedSearchControl.getCookieValue();
467                PagedSearchContext psCookie =  session.removePagedSearchContext( cookieValue );
468                pagedResultsControl = new PagedResultsControl( 0, psCookie.getCookie(), true );
469                
470                // Close the cursor
471                EntryFilteringCursor cursor = psCookie.getCursor();
472                
473                if ( cursor != null )
474                {
475                    cursor.close();
476                }
477            }
478            else
479            {
480                pagedResultsControl = new PagedResultsControl( 0, true );
481            }
482            
483            // and return
484            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
485            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
486            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
487            req.getResultResponse().add( pagedResultsControl );
488            return ( InternalSearchResponseDone ) req.getResultResponse();
489        }
490        
491        
492        /**
493         * Remove a cookie instance from the session, if it exists.
494         */
495        private PagedSearchContext removeContext( LdapSession session, PagedSearchContext cookieInstance )
496        {
497            if ( cookieInstance == null )
498            {
499                return null;
500            }
501            
502            int cookieValue = cookieInstance.getCookieValue();
503            
504            return session.removePagedSearchContext( cookieValue );
505        }
506        
507        
508        /**
509         * Handle a Paged Search request.
510         */
511        private InternalSearchResponseDone doPagedSearch( LdapSession session, InternalSearchRequest req, PagedSearchControl control )
512            throws Exception
513        {
514            PagedSearchControl pagedSearchControl = ( PagedSearchControl )control;
515            PagedResultsControl pagedResultsControl = null;
516    
517            // Get the size limits
518            // Don't bother setting size limits for administrators that don't ask for it
519            int serverLimit = getServerSizeLimit( session, req );
520            
521            int requestLimit = req.getSizeLimit() == 0 ?
522                Integer.MAX_VALUE : req.getSizeLimit();
523            int sizeLimit = min( serverLimit, requestLimit );
524    
525            int pagedLimit = pagedSearchControl.getSize();
526            EntryFilteringCursor cursor = null;
527            PagedSearchContext pagedContext = null;
528    
529            // We have the following cases :
530            // 1) The SIZE is 0 and the cookie is the same than the previous one : this
531            // is a abandon request for this paged search.
532            // 2) The cookie is empty : this is a new request. If the requested
533            // size is above the serverLimit and the request limit, this is a normal
534            // search
535            // 3) The cookie is not empty and the request is the same, we return
536            // the next SIZE elements
537            // 4) The cookie is not empty, but the request is not the same : this is 
538            // a new request (we have to discard the cookie and do a new search from
539            // the beginning)
540            // 5) The SIZE is above the size-limit : the request is treated as if it
541            // was a simple search
542            
543            // Case 1
544            if ( pagedLimit == 0 )
545            {
546                // An abandoned paged search
547                return abandonPagedSearch( session, req );
548            }
549            
550            // Now, depending on the cookie, we will deal with case 2, 3, 4 and 5
551            byte [] cookie= pagedSearchControl.getCookie();
552            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
553            
554            if ( StringTools.isEmpty( cookie ) )
555            {
556                // This is a new search. We have a special case when the paged size
557                // is above the server size limit : in this case, we default to a 
558                // standard search
559                if ( pagedLimit > sizeLimit )
560                {
561                    // Normal search : create the cursor, and set pagedControl to false
562                    try
563                    {
564                        // No cursor : do a search.
565                        cursor = session.getCoreSession().search( req );
566    
567                        // Position the cursor at the beginning
568                        cursor.beforeFirst();
569                        
570                        // And read the entries
571                        readResults( session, req, ldapResult, cursor, sizeLimit );
572                    }
573                    finally
574                    {
575                        try
576                        {
577                            cursor.close();
578                        }
579                        catch ( NamingException e )
580                        {
581                            LOG.error( "failed on list.close()", e );
582                        }
583                    }
584                    
585                    // If we had a cookie in the session, remove it
586                    removeContext( session, pagedContext );
587                    return ( InternalSearchResponseDone ) req.getResultResponse();
588                }
589                else
590                {
591                    // Case 2 : create the context
592                    pagedContext = new PagedSearchContext( req );
593    
594                    session.addPagedSearchContext( pagedContext );
595                    cookie = pagedContext.getCookie();
596                    pagedResultsControl = new PagedResultsControl( 0, cookie, true );
597    
598                    // No cursor : do a search.
599                    cursor = session.getCoreSession().search( req );
600    
601                    // Position the cursor at the beginning
602                    cursor.beforeFirst();
603                    
604                    // And stores the cursor into the session
605                    pagedContext.setCursor( cursor );
606                }
607            }
608            else
609            {
610                // We have a cookie
611                // Either case 3, 4 or 5
612                int cookieValue = pagedSearchControl.getCookieValue();
613                pagedContext = session.getPagedSearchContext( cookieValue );
614                
615                if ( pagedContext == null )
616                {
617                    // We didn't found the cookie into the session : it must be invalid
618                    // send an error.
619                    ldapResult.setErrorMessage( "Invalid cookie for this PagedSearch request." );
620                    ldapResult.setResultCode( ResultCodeEnum.UNWILLING_TO_PERFORM );
621                    
622                    return ( InternalSearchResponseDone ) req.getResultResponse();
623                }
624                
625                if ( pagedContext.hasSameRequest( req, session ) )
626                {
627                    // Case 3 : continue the search
628                    cursor = pagedContext.getCursor();
629                    
630                    // get the cookie
631                    cookie = pagedContext.getCookie();
632                    pagedResultsControl = new PagedResultsControl( 0, cookie, true );
633                }
634                else
635                {
636                    // case 2 : create a new cursor
637                    // We have to close the cursor
638                    cursor = pagedContext.getCursor();
639                    
640                    if ( cursor != null )
641                    {
642                        cursor.close();
643                    }
644                    
645                    // Now create a new context and stores it into the session
646                    pagedContext = new PagedSearchContext( req );
647    
648                    session.addPagedSearchContext( pagedContext );
649                    
650                    cookie = pagedContext.getCookie();
651                    pagedResultsControl = new PagedResultsControl( 0, cookie, true );
652                }
653            }
654            
655            // Now, do the real search
656            /*
657             * Iterate through all search results building and sending back responses
658             * for each search result returned.
659             */
660            try
661            {
662                readPagedResults( session, req, ldapResult, cursor, sizeLimit, pagedLimit, true, pagedContext, pagedResultsControl );
663            }
664            catch ( Exception e )
665            {
666                if ( cursor != null )
667                {
668                    try
669                    {
670                        cursor.close();
671                    }
672                    catch ( NamingException ne )
673                    {
674                        LOG.error( "failed on list.close()", ne );
675                    }
676                }
677            }
678            
679            return ( InternalSearchResponseDone ) req.getResultResponse();
680        }
681    
682        
683        /**
684         * Conducts a simple search across the result set returning each entry 
685         * back except for the search response done.  This is calculated but not
686         * returned so the persistent search mechanism can leverage this method
687         * along with standard search.<br>
688         * <br>
689         * @param session the LDAP session object for this request
690         * @param req the search request 
691         * @return the result done 
692         * @throws Exception if there are failures while processing the request
693         */
694        private InternalSearchResponseDone doSimpleSearch( LdapSession session, InternalSearchRequest req ) 
695            throws Exception
696        {
697            InternalLdapResult ldapResult = req.getResultResponse().getLdapResult();
698            
699            // Check if we are using the Paged Search Control
700            Object control = req.getControls().get( PagedSearchControl.CONTROL_OID );
701            
702            if ( control != null )
703            {
704                // Let's deal with the pagedControl
705                return doPagedSearch( session, req, (PagedSearchControl)control );
706            }
707            
708            // A normal search
709            // Check that we have a cursor or not. 
710            // No cursor : do a search.
711            EntryFilteringCursor cursor = session.getCoreSession().search( req );
712    
713            // Position the cursor at the beginning
714            cursor.beforeFirst();
715            
716            /*
717             * Iterate through all search results building and sending back responses
718             * for each search result returned.
719             */
720            try
721            {
722                // Get the size limits
723                // Don't bother setting size limits for administrators that don't ask for it
724                int serverLimit = getServerSizeLimit( session, req );
725                
726                int requestLimit = req.getSizeLimit() == 0 ?
727                    Integer.MAX_VALUE : req.getSizeLimit();
728    
729                req.addAbandonListener( new SearchAbandonListener( ldapServer, cursor ) );
730                setTimeLimitsOnCursor( req, session, cursor );
731                LOG.debug( "using <{},{}> for size limit", requestLimit, serverLimit );
732                int sizeLimit = min( requestLimit, serverLimit );
733                
734                readResults( session, req, ldapResult, cursor, sizeLimit );
735            }
736            finally
737            {
738                if ( cursor != null )
739                {
740                    try
741                    {
742                        cursor.close();
743                    }
744                    catch ( NamingException e )
745                    {
746                        LOG.error( "failed on list.close()", e );
747                    }
748                }
749            }
750            
751            return ( InternalSearchResponseDone ) req.getResultResponse();
752        }
753        
754    
755        /**
756         * Generates a response for an entry retrieved from the server core based 
757         * on the nature of the request with respect to referral handling.  This 
758         * method will either generate a SearchResponseEntry or a 
759         * SearchResponseReference depending on if the entry is a referral or if 
760         * the ManageDSAITControl has been enabled.
761         *
762         * @param req the search request
763         * @param entry the entry to be handled
764         * @return the response for the entry
765         * @throws Exception if there are problems in generating the response
766         */
767        private InternalResponse generateResponse( LdapSession session, InternalSearchRequest req, ClonedServerEntry entry ) throws Exception
768        {
769            EntryAttribute ref = entry.getOriginalEntry().get( SchemaConstants.REF_AT );
770            boolean hasManageDsaItControl = req.getControls().containsKey( ManageDsaITControl.CONTROL_OID );
771    
772            if ( ( ref != null ) && ! hasManageDsaItControl )
773            {
774                // The entry is a referral.
775                InternalSearchResponseReference respRef;
776                respRef = new SearchResponseReferenceImpl( req.getMessageId() );
777                respRef.setReferral( new ReferralImpl() );
778                
779                for ( Value<?> val : ref )
780                {
781                    String url = val.getString();
782                    
783                    if ( ! url.startsWith( "ldap" ) )
784                    {
785                        respRef.getReferral().addLdapUrl( url );
786                    }
787                    
788                    LdapURL ldapUrl = new LdapURL();
789                    ldapUrl.setForceScopeRendering( true );
790                    try
791                    {
792                        ldapUrl.parse( url.toCharArray() );
793                    }
794                    catch ( LdapURLEncodingException e )
795                    {
796                        LOG.error( "Bad URL ({}) for ref in {}.  Reference will be ignored.", url, entry );
797                    }
798    
799                    switch( req.getScope() )
800                    {
801                        case SUBTREE:
802                            ldapUrl.setScope( SearchScope.SUBTREE.getJndiScope() );
803                            break;
804                            
805                        case ONELEVEL: // one level here is object level on remote server
806                            ldapUrl.setScope( SearchScope.OBJECT.getJndiScope() );
807                            break;
808                            
809                        default:
810                            throw new IllegalStateException( "Unexpected base scope." );
811                    }
812                    
813                    respRef.getReferral().addLdapUrl( ldapUrl.toString() );
814                }
815                
816                return respRef;
817            }
818            else 
819            {
820                // The entry is not a referral, or the ManageDsaIt control is set
821                InternalSearchResponseEntry respEntry;
822                respEntry = new SearchResponseEntryImpl( req.getMessageId() );
823                respEntry.setEntry( entry );
824                respEntry.setObjectName( entry.getDn() );
825                
826                // Filter the userPassword if the server mandate to do so
827                if ( session.getCoreSession().getDirectoryService().isPasswordHidden() )
828                {
829                    // Remove the userPassord attribute from the entry.
830                    respEntry.getEntry().removeAttributes( SchemaConstants.USER_PASSWORD_AT );
831                }
832                
833                return respEntry;
834            }
835        }
836        
837        
838        /**
839         * Alters the filter expression based on the presence of the 
840         * ManageDsaIT control.  If the control is not present, the search
841         * filter will be altered to become a disjunction with two terms.
842         * The first term is the original filter.  The second term is a
843         * (objectClass=referral) assertion.  When OR'd together these will
844         * make sure we get all referrals so we can process continuations 
845         * properly without having the filter remove them from the result 
846         * set.
847         * 
848         * NOTE: original filter is first since most entries are not referrals 
849         * so it has a higher probability on average of accepting and shorting 
850         * evaluation before having to waste cycles trying to evaluate if the 
851         * entry is a referral.
852         *
853         * @param session the session to use to construct the filter (schema access)
854         * @param req the request to get the original filter from
855         * @throws Exception if there are schema access problems
856         */
857        public void modifyFilter( LdapSession session, InternalSearchRequest req ) throws Exception
858        {
859            if ( req.hasControl( ManageDsaITControl.CONTROL_OID ) )
860            {
861                return;
862            }
863            
864            /*
865             * Do not add the OR'd (objectClass=referral) expression if the user 
866             * searches for the subSchemaSubEntry as the SchemaIntercepter can't 
867             * handle an OR'd filter.
868             */
869            if ( isSubSchemaSubEntrySearch( session, req ) )
870            {
871                return;
872            }
873            
874            /*
875             * Most of the time the search filter is just (objectClass=*) and if 
876             * this is the case then there's no reason at all to OR this with an
877             * (objectClass=referral).  If we detect this case then we leave it 
878             * as is to represent the OR condition:
879             * 
880             *  (| (objectClass=referral)(objectClass=*)) == (objectClass=*)
881             */
882            if ( req.getFilter() instanceof PresenceNode )
883            {
884                PresenceNode presenceNode = ( PresenceNode ) req.getFilter();
885                
886                AttributeType at = session.getCoreSession().getDirectoryService()
887                    .getRegistries().getAttributeTypeRegistry().lookup( presenceNode.getAttribute() );
888                if ( at.getOid().equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
889                {
890                    return;
891                }
892            }
893    
894            // using varags to add two expressions to an OR node 
895            req.setFilter( new OrNode( req.getFilter(), newIsReferralEqualityNode( session ) ) );
896        }
897        
898        
899        /**
900         * Main message handing method for search requests.  This will be called 
901         * even if the ManageDsaIT control is present because the super class does
902         * not know that the search operation has more to do after finding the 
903         * base.  The call to this means that finding the base can ignore 
904         * referrals.
905         * 
906         * @param session the associated session
907         * @param req the received SearchRequest
908         */
909        public void handleIgnoringReferrals( LdapSession session, InternalSearchRequest req )
910        {
911            if ( IS_DEBUG )
912            {
913                LOG.debug( "Message received:  {}", req.toString() );
914            }
915    
916            // A flag set if we have a persistent search
917            boolean isPersistentSearch = false;
918            
919            // A flag set when we've got an exception while processing a
920            // persistent search
921            boolean persistentSearchException = false;
922            
923            // add the search request to the registry of outstanding requests for this session
924            session.registerOutstandingRequest( req );
925    
926            try
927            {
928                // ===============================================================
929                // Handle search in rootDSE differently.
930                // ===============================================================
931                if ( isRootDSESearch( req ) )
932                {
933                    handleRootDseSearch( session, req );
934                    
935                    return;
936                }
937    
938                // modify the filter to affect continuation support
939                modifyFilter( session, req );
940                
941                // ===============================================================
942                // Handle psearch differently
943                // ===============================================================
944    
945                PersistentSearchControl psearchControl = ( PersistentSearchControl ) 
946                    req.getControls().get( PersistentSearchControl.CONTROL_OID );
947                
948                if ( psearchControl != null )
949                {
950                    // Set the flag to avoid the request being removed
951                    // from the session
952                    isPersistentSearch = true;
953    
954                    handlePersistentSearch( session, req, psearchControl );
955                    
956                    return;
957                }
958    
959                // ===============================================================
960                // Handle regular search requests from here down
961                // ===============================================================
962    
963                InternalSearchResponseDone done = doSimpleSearch( session, req );
964                session.getIoSession().write( done );
965            }
966            catch ( Exception e )
967            {
968                /*
969                 * From RFC 2251 Section 4.11:
970                 *
971                 * In the event that a server receives an Abandon Request on a Search
972                 * operation in the midst of transmitting responses to the Search, that
973                 * server MUST cease transmitting entry responses to the abandoned
974                 * request immediately, and MUST NOT send the SearchResultDone. Of
975                 * course, the server MUST ensure that only properly encoded LDAPMessage
976                 * PDUs are transmitted.
977                 *
978                 * SO DON'T SEND BACK ANYTHING!!!!!
979                 */
980                if ( e instanceof OperationAbandonedException )
981                {
982                    return;
983                }
984    
985                // If it was a persistent search and if we had an exception,
986                // we set the flag to remove the request from the session
987                if ( isPersistentSearch )
988                {
989                    persistentSearchException = true;
990                }
991                
992                handleException( session, req, e );
993            }
994            finally 
995            {
996                
997                // remove the request from the session, except if
998                // we didn't got an exception for a Persistent search 
999                if ( !isPersistentSearch || persistentSearchException )
1000                {
1001                    session.unregisterOutstandingRequest( req );
1002                }
1003            }
1004        }
1005    
1006    
1007        /**
1008         * Handles processing with referrals without ManageDsaIT control.
1009         */
1010        public void handleWithReferrals( LdapSession session, LdapDN reqTargetDn, InternalSearchRequest req ) throws NamingException
1011        {
1012            InternalLdapResult result = req.getResultResponse().getLdapResult();
1013            ClonedServerEntry entry = null;
1014            boolean isReferral = false;
1015            boolean isparentReferral = false;
1016            ReferralManager referralManager = session.getCoreSession().getDirectoryService().getReferralManager();
1017            
1018            reqTargetDn.normalize( session.getCoreSession().getDirectoryService().getRegistries().getAttributeTypeRegistry().getNormalizerMapping() );
1019            
1020            // Check if the entry itself is a referral
1021            referralManager.lockRead();
1022            
1023            isReferral = referralManager.isReferral( reqTargetDn );
1024            
1025            if ( !isReferral )
1026            {
1027                // Check if the entry has a parent which is a referral
1028                isparentReferral = referralManager.hasParentReferral( reqTargetDn );
1029            }
1030            
1031            referralManager.unlock();
1032            
1033            if ( !isReferral && !isparentReferral )
1034            {
1035                // This is not a referral and it does not have a parent which 
1036                // is a referral : standard case, just deal with the request
1037                LOG.debug( "Entry {} is NOT a referral.", reqTargetDn );
1038                handleIgnoringReferrals( session, req );
1039                return;
1040            }
1041            else
1042            {
1043                // -------------------------------------------------------------------
1044                // Lookup Entry
1045                // -------------------------------------------------------------------
1046                
1047                // try to lookup the entry but ignore exceptions when it does not   
1048                // exist since entry may not exist but may have an ancestor that is a 
1049                // referral - would rather attempt a lookup that fails then do check 
1050                // for existence than have to do another lookup to get entry info
1051                try
1052                {
1053                    entry = session.getCoreSession().lookup( reqTargetDn );
1054                    LOG.debug( "Entry for {} was found: ", reqTargetDn, entry );
1055                }
1056                catch ( NameNotFoundException e )
1057                {
1058                    /* ignore */
1059                    LOG.debug( "Entry for {} not found.", reqTargetDn );
1060                }
1061                catch ( Exception e )
1062                {
1063                    /* serious and needs handling */
1064                    handleException( session, req, e );
1065                    return;
1066                }
1067                
1068                // -------------------------------------------------------------------
1069                // Handle Existing Entry
1070                // -------------------------------------------------------------------
1071                
1072                if ( entry != null )
1073                {
1074                    try
1075                    {
1076                        LOG.debug( "Entry is a referral: {}", entry );
1077                        
1078                        handleReferralEntryForSearch( session, ( InternalSearchRequest ) req, entry );
1079    
1080                        return;
1081                    }
1082                    catch ( Exception e )
1083                    {
1084                        handleException( session, req, e );
1085                    }
1086                }
1087        
1088                // -------------------------------------------------------------------
1089                // Handle Non-existing Entry
1090                // -------------------------------------------------------------------
1091                
1092                // if the entry is null we still have to check for a referral ancestor
1093                // also the referrals need to be adjusted based on the ancestor's ref
1094                // values to yield the correct path to the entry in the target DSAs
1095                
1096                else
1097                {
1098                    // The entry is null : it has a parent referral.
1099                    ClonedServerEntry referralAncestor = null;
1100        
1101                    try
1102                    {
1103                        referralAncestor = getFarthestReferralAncestor( session, reqTargetDn );
1104                    }
1105                    catch ( Exception e )
1106                    {
1107                        handleException( session, req, e );
1108                        return;
1109                    }
1110        
1111                    if ( referralAncestor == null )
1112                    {
1113                        result.setErrorMessage( "Entry not found." );
1114                        result.setResultCode( ResultCodeEnum.NO_SUCH_OBJECT );
1115                        session.getIoSession().write( req.getResultResponse() );
1116                        return;
1117                    }
1118                      
1119                    // if we get here then we have a valid referral ancestor
1120                    try
1121                    {
1122                        InternalReferral referral = getReferralOnAncestorForSearch( session, ( InternalSearchRequest ) req, referralAncestor );
1123                        
1124                        result.setResultCode( ResultCodeEnum.REFERRAL );
1125                        result.setReferral( referral );
1126                        session.getIoSession().write( req.getResultResponse() );
1127                    }
1128                    catch ( Exception e )
1129                    {
1130                        handleException( session, req, e );
1131                    }
1132                }
1133            }
1134        }
1135        
1136        
1137        /**
1138         * Handles processing a referral response on a target entry which is a 
1139         * referral.  It will for any request that returns an LdapResult in it's 
1140         * response.
1141         *
1142         * @param session the session to use for processing
1143         * @param reqTargetDn the dn of the target entry of the request
1144         * @param req the request
1145         * @param entry the entry associated with the request
1146         */
1147        private void handleReferralEntryForSearch( LdapSession session, InternalSearchRequest req, ClonedServerEntry entry )
1148            throws Exception
1149        {
1150            InternalLdapResult result = req.getResultResponse().getLdapResult();
1151            ReferralImpl referral = new ReferralImpl();
1152            result.setReferral( referral );
1153            result.setResultCode( ResultCodeEnum.REFERRAL );
1154            result.setErrorMessage( "Encountered referral attempting to handle request." );
1155            result.setMatchedDn( req.getBase() );
1156    
1157            EntryAttribute refAttr = entry.getOriginalEntry().get( SchemaConstants.REF_AT );
1158            
1159            for ( Value<?> refval : refAttr )
1160            {
1161                String refstr = refval.getString();
1162                
1163                // need to add non-ldap URLs as-is
1164                if ( ! refstr.startsWith( "ldap" ) )
1165                {
1166                    referral.addLdapUrl( refstr );
1167                    continue;
1168                }
1169                
1170                // parse the ref value and normalize the DN  
1171                LdapURL ldapUrl = new LdapURL();
1172                try
1173                {
1174                    ldapUrl.parse( refstr.toCharArray() );
1175                }
1176                catch ( LdapURLEncodingException e )
1177                {
1178                    LOG.error( "Bad URL ({}) for ref in {}.  Reference will be ignored.", refstr, entry );
1179                    continue;
1180                }
1181                
1182                ldapUrl.setForceScopeRendering( true );
1183                ldapUrl.setAttributes( req.getAttributes() );
1184                ldapUrl.setScope( req.getScope().getJndiScope() );
1185                referral.addLdapUrl( ldapUrl.toString() );
1186            }
1187    
1188            session.getIoSession().write( req.getResultResponse() );
1189        }
1190        
1191        
1192        /**
1193         * Determines if a search request is on the RootDSE of the server.
1194         * 
1195         * It is a RootDSE search if :
1196         * - the base DN is empty
1197         * - and the scope is BASE OBJECT
1198         * - and the filter is (ObjectClass = *)
1199         * 
1200         * (RFC 4511, 5.1, par. 1 & 2)
1201         *
1202         * @param req the request issued
1203         * @return true if the search is on the RootDSE false otherwise
1204         */
1205        private static boolean isRootDSESearch( InternalSearchRequest req )
1206        {
1207            boolean isBaseIsRoot = req.getBase().isEmpty();
1208            boolean isBaseScope = req.getScope() == SearchScope.OBJECT;
1209            boolean isRootDSEFilter = false;
1210            
1211            if ( req.getFilter() instanceof PresenceNode )
1212            {
1213                String attribute = ( ( PresenceNode ) req.getFilter() ).getAttribute();
1214                isRootDSEFilter = attribute.equalsIgnoreCase( SchemaConstants.OBJECT_CLASS_AT ) ||
1215                                    attribute.equals( SchemaConstants.OBJECT_CLASS_AT_OID );
1216            }
1217            
1218            return isBaseIsRoot && isBaseScope && isRootDSEFilter;
1219        }
1220        
1221        
1222        /**
1223         * <p>
1224         * Determines if a search request is a subSchemaSubEntry search.
1225         * </p>
1226         * <p>
1227         * It is a schema search if:
1228         * - the base DN is the DN of the subSchemaSubEntry of the root DSE
1229         * - and the scope is BASE OBJECT
1230         * - and the filter is (objectClass=subschema)
1231         * (RFC 4512, 4.4,)
1232         * </p>
1233         * <p>
1234         * However in this method we only check the first condition to avoid
1235         * performance issues.
1236         * </p>
1237         * 
1238         * @param session the LDAP session
1239         * @param req the request issued
1240         * 
1241         * @return true if the search is on the subSchemaSubEntry, false otherwise
1242         * 
1243         * @throws Exception the exception
1244         */
1245        private static boolean isSubSchemaSubEntrySearch( LdapSession session, InternalSearchRequest req ) throws Exception
1246        {
1247            LdapDN base = req.getBase();
1248            String baseNormForm = ( base.isNormalized() ? base.getNormName() : base.toNormName() );
1249    
1250            DirectoryService ds = session.getCoreSession().getDirectoryService();
1251            PartitionNexus nexus = ds.getPartitionNexus();
1252            Value<?> subschemaSubentry = nexus.getRootDSE( null ).get( SchemaConstants.SUBSCHEMA_SUBENTRY_AT ).get();
1253            LdapDN subschemaSubentryDn = new LdapDN( subschemaSubentry.getString() );
1254            subschemaSubentryDn.normalize( ds.getRegistries().getAttributeTypeRegistry().getNormalizerMapping() );
1255            String subschemaSubentryDnNorm = subschemaSubentryDn.getNormName();
1256            
1257            return subschemaSubentryDnNorm.equals( baseNormForm );
1258        }
1259    }