001    /*
002     * Copyright 2009-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2009-2013 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.util.concurrent.LinkedBlockingQueue;
026    import java.util.concurrent.TimeUnit;
027    import java.util.concurrent.atomic.AtomicBoolean;
028    import java.util.concurrent.atomic.AtomicReference;
029    
030    import com.unboundid.util.InternalUseOnly;
031    import com.unboundid.util.ThreadSafety;
032    import com.unboundid.util.ThreadSafetyLevel;
033    
034    import static com.unboundid.ldap.sdk.LDAPMessages.*;
035    import static com.unboundid.util.Debug.*;
036    import static com.unboundid.util.Validator.*;
037    
038    
039    
040    /**
041     * This class provides an {@link EntrySource} that will read entries matching a
042     * given set of search criteria from an LDAP directory server.  It may
043     * optionally close the associated connection after all entries have been read.
044     * <BR><BR>
045     * This implementation processes the search asynchronously, which provides two
046     * benefits:
047     * <UL>
048     *   <LI>It makes it easier to provide a throttling mechanism to prevent the
049     *       entries from piling up and causing the client to run out of memory if
050     *       the server returns them faster than the client can process them.  If
051     *       this occurs, then the client will queue up a small number of entries
052     *       but will then push back against the server to block it from sending
053     *       additional entries until the client can catch up.  In this case, no
054     *       entries should be lost, although some servers may impose limits on how
055     *       long a search may be active or other forms of constraints.</LI>
056     *   <LI>It makes it possible to abandon the search if the entry source is no
057     *       longer needed (as signified by calling the {@link #close} method) and
058     *       the caller intends to stop iterating through the results.</LI>
059     * </UL>
060     * <H2>Example</H2>
061     * The following example demonstrates the process that may be used for iterating
062     * across all entries containing the {@code person} object class using the LDAP
063     * entry source API:
064     * <PRE>
065     *   SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
066     *        SearchScope.SUB, "(objectClass=person)");
067     *   LDAPEntrySource entrySource = new LDAPEntrySource(connection,
068     *        searchRequest, false);
069     *
070     *   try
071     *   {
072     *     while (true)
073     *     {
074     *       try
075     *       {
076     *         Entry entry = entrySource.nextEntry();
077     *         if (entry == null)
078     *         {
079     *           // There are no more entries to be read.
080     *           break;
081     *         }
082     *         else
083     *         {
084     *           // Do something with the entry here.
085     *         }
086     *       }
087     *       catch (SearchResultReferenceEntrySourceException e)
088     *       {
089     *         // The directory server returned a search result reference.
090     *         SearchResultReference searchReference = e.getSearchReference();
091     *       }
092     *       catch (EntrySourceException e)
093     *       {
094     *         // Some kind of problem was encountered (e.g., the connection is no
095     *         // longer valid).  See if we can continue reading entries.
096     *         if (! e.mayContinueReading())
097     *         {
098     *           break;
099     *         }
100     *       }
101     *     }
102     *   }
103     *   finally
104     *   {
105     *     entrySource.close();
106     *   }
107     * </PRE>
108     */
109    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
110    public final class LDAPEntrySource
111           extends EntrySource
112           implements AsyncSearchResultListener
113    {
114      /**
115       * The bogus entry that will be used to signify the end of the results.
116       */
117      private static final String END_OF_RESULTS = "END OF RESULTS";
118    
119    
120    
121      /**
122       * The serial version UID for this serializable class.
123       */
124      private static final long serialVersionUID = 1080386705549149135L;
125    
126    
127    
128      // The request ID associated with the asynchronous search.
129      private final AsyncRequestID asyncRequestID;
130    
131      // Indicates whether this entry source has been closed.
132      private final AtomicBoolean closed;
133    
134      // The search result for the search operation.
135      private final AtomicReference<SearchResult> searchResult;
136    
137      // Indicates whether to close the connection when this entry source is closed.
138      private final boolean closeConnection;
139    
140      // The connection that will be used to read the entries.
141      private final LDAPConnection connection;
142    
143      // The queue from which entries will be read.
144      private final LinkedBlockingQueue<Object> queue;
145    
146    
147    
148      /**
149       * Creates a new LDAP entry source with the provided information.
150       *
151       * @param  connection       The connection to the directory server from which
152       *                          the entries will be read.  It must not be
153       *                          {@code null}.
154       * @param  searchRequest    The search request that will be used to identify
155       *                          which entries should be returned.  It must not be
156       *                          {@code null}, and it must not be configured with a
157       *                          {@link SearchResultListener}.
158       * @param  closeConnection  Indicates whether the provided connection should
159       *                          be closed whenever all of the entries have been
160       *                          read, or if the {@link #close} method is called.
161       *
162       * @throws  LDAPException  If there is a problem with the provided search
163       *                         request or when trying to communicate with the
164       *                         directory server over the provided connection.
165       */
166      public LDAPEntrySource(final LDAPConnection connection,
167                             final SearchRequest searchRequest,
168                             final boolean closeConnection)
169             throws LDAPException
170      {
171        this(connection, searchRequest, closeConnection, 100);
172      }
173    
174    
175    
176      /**
177       * Creates a new LDAP entry source with the provided information.
178       *
179       * @param  connection       The connection to the directory server from which
180       *                          the entries will be read.  It must not be
181       *                          {@code null}.
182       * @param  searchRequest    The search request that will be used to identify
183       *                          which entries should be returned.  It must not be
184       *                          {@code null}, and it must not be configured with a
185       *                          {@link SearchResultListener}.
186       * @param  closeConnection  Indicates whether the provided connection should
187       *                          be closed whenever all of the entries have been
188       *                          read, or if the {@link #close} method is called.
189       * @param  queueSize        The size of the internal queue used to hold search
190       *                          result entries until they can be consumed by the
191       *                          {@link #nextEntry} method.  The value must be
192       *                          greater than zero.
193       *
194       * @throws  LDAPException  If there is a problem with the provided search
195       *                         request or when trying to communicate with the
196       *                         directory server over the provided connection.
197       */
198      public LDAPEntrySource(final LDAPConnection connection,
199                             final SearchRequest searchRequest,
200                             final boolean closeConnection,
201                             final int queueSize)
202             throws LDAPException
203      {
204        ensureNotNull(connection, searchRequest);
205        ensureTrue(queueSize > 0,
206                   "LDAPEntrySource.queueSize must be greater than 0.");
207    
208        this.connection      = connection;
209        this.closeConnection = closeConnection;
210    
211        if (searchRequest.getSearchResultListener() != null)
212        {
213          throw new LDAPException(ResultCode.PARAM_ERROR,
214                                  ERR_LDAP_ENTRY_SOURCE_REQUEST_HAS_LISTENER.get());
215        }
216    
217        closed       = new AtomicBoolean(false);
218        searchResult = new AtomicReference<SearchResult>();
219        queue        = new LinkedBlockingQueue<Object>(queueSize);
220    
221        final SearchRequest r = new SearchRequest(this, searchRequest.getControls(),
222             searchRequest.getBaseDN(), searchRequest.getScope(),
223             searchRequest.getDereferencePolicy(), searchRequest.getSizeLimit(),
224             searchRequest.getTimeLimitSeconds(), searchRequest.typesOnly(),
225             searchRequest.getFilter(), searchRequest.getAttributes());
226        asyncRequestID = connection.asyncSearch(r);
227      }
228    
229    
230    
231      /**
232       * {@inheritDoc}
233       */
234      @Override()
235      public Entry nextEntry()
236             throws EntrySourceException
237      {
238        while (true)
239        {
240          if (closed.get() && queue.isEmpty())
241          {
242            return null;
243          }
244    
245          final Object o;
246          try
247          {
248            o = queue.poll(10L, TimeUnit.MILLISECONDS);
249          }
250          catch (InterruptedException ie)
251          {
252            debugException(ie);
253            continue;
254          }
255    
256          if (o != null)
257          {
258            if (o == END_OF_RESULTS)
259            {
260              return null;
261            }
262            else if (o instanceof Entry)
263            {
264              return (Entry) o;
265            }
266            else
267            {
268              throw (EntrySourceException) o;
269            }
270          }
271        }
272      }
273    
274    
275    
276      /**
277       * {@inheritDoc}
278       */
279      @Override()
280      public void close()
281      {
282        closeInternal(true);
283      }
284    
285    
286    
287      /**
288       * Closes this LDAP entry source.
289       *
290       * @param  abandon  Indicates whether to attempt to abandon the search.
291       */
292      private void closeInternal(final boolean abandon)
293      {
294        addToQueue(END_OF_RESULTS);
295    
296        if (closed.compareAndSet(false, true))
297        {
298          if (abandon)
299          {
300            try
301            {
302              connection.abandon(asyncRequestID);
303            }
304            catch (Exception e)
305            {
306              debugException(e);
307            }
308          }
309    
310          if (closeConnection)
311          {
312            connection.close();
313          }
314        }
315      }
316    
317    
318    
319      /**
320       * Retrieves the search result for the search operation, if available.  It
321       * will not be available until the search has completed (as indicated by a
322       * {@code null} return value from the {@link #nextEntry} method).
323       *
324       * @return  The search result for the search operation, or {@code null} if it
325       *          is not available (e.g., because the search has not yet completed).
326       */
327      public SearchResult getSearchResult()
328      {
329        return searchResult.get();
330      }
331    
332    
333    
334      /**
335       * {@inheritDoc}  This is intended for internal use only and should not be
336       * called by anything outside of the LDAP SDK itself.
337       */
338      @InternalUseOnly()
339      public void searchEntryReturned(final SearchResultEntry searchEntry)
340      {
341        addToQueue(searchEntry);
342      }
343    
344    
345    
346      /**
347       * {@inheritDoc}  This is intended for internal use only and should not be
348       * called by anything outside of the LDAP SDK itself.
349       */
350      @InternalUseOnly()
351      public void searchReferenceReturned(
352                       final SearchResultReference searchReference)
353      {
354        addToQueue(new SearchResultReferenceEntrySourceException(searchReference));
355      }
356    
357    
358    
359      /**
360       * {@inheritDoc}  This is intended for internal use only and should not be
361       * called by anything outside of the LDAP SDK itself.
362       */
363      @InternalUseOnly()
364      public void searchResultReceived(final AsyncRequestID requestID,
365                                       final SearchResult searchResult)
366      {
367        this.searchResult.set(searchResult);
368    
369        if (! searchResult.getResultCode().equals(ResultCode.SUCCESS))
370        {
371          addToQueue(new EntrySourceException(false,
372               new LDAPSearchException(searchResult)));
373        }
374    
375        closeInternal(false);
376      }
377    
378    
379    
380      /**
381       * Adds the provided object to the queue, waiting as long as needed until it
382       * has been added.
383       *
384       * @param  o  The object to be added.  It must not be {@code null}.
385       */
386      private void addToQueue(final Object o)
387      {
388        while (true)
389        {
390          if (closed.get())
391          {
392            return;
393          }
394    
395          try
396          {
397            if (queue.offer(o, 100L, TimeUnit.MILLISECONDS))
398            {
399              return;
400            }
401          }
402          catch (InterruptedException ie)
403          {
404            debugException(ie);
405          }
406        }
407      }
408    }