001/*
002 * Copyright 2009-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2009-2019 Ping Identity Corporation
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 */
021package com.unboundid.ldap.sdk.migrate.ldapjdk;
022
023
024
025import java.util.Enumeration;
026import java.util.NoSuchElementException;
027import java.util.concurrent.LinkedBlockingQueue;
028import java.util.concurrent.TimeUnit;
029import java.util.concurrent.atomic.AtomicBoolean;
030import java.util.concurrent.atomic.AtomicInteger;
031import java.util.concurrent.atomic.AtomicReference;
032
033import com.unboundid.ldap.sdk.AsyncRequestID;
034import com.unboundid.ldap.sdk.AsyncSearchResultListener;
035import com.unboundid.ldap.sdk.Control;
036import com.unboundid.ldap.sdk.ResultCode;
037import com.unboundid.ldap.sdk.SearchResult;
038import com.unboundid.ldap.sdk.SearchResultEntry;
039import com.unboundid.ldap.sdk.SearchResultReference;
040import com.unboundid.util.Debug;
041import com.unboundid.util.InternalUseOnly;
042import com.unboundid.util.Mutable;
043import com.unboundid.util.NotExtensible;
044import com.unboundid.util.ThreadSafety;
045import com.unboundid.util.ThreadSafetyLevel;
046
047
048
049/**
050 * This class provides a data structure that provides access to data returned
051 * in response to a search operation.
052 * <BR><BR>
053 * This class is primarily intended to be used in the process of updating
054 * applications which use the Netscape Directory SDK for Java to switch to or
055 * coexist with the UnboundID LDAP SDK for Java.  For applications not written
056 * using the Netscape Directory SDK for Java, the {@link SearchResult} class
057 * should be used instead.
058 */
059@Mutable()
060@NotExtensible()
061@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
062public class LDAPSearchResults
063       implements Enumeration<Object>, AsyncSearchResultListener
064{
065  /**
066   * The serial version UID for this serializable class.
067   */
068  private static final long serialVersionUID = 7884355145560496230L;
069
070
071
072  // The asynchronous request ID for these search results.
073  private volatile AsyncRequestID asyncRequestID;
074
075  // Indicates whether the search has been abandoned.
076  private final AtomicBoolean searchAbandoned;
077
078  // Indicates whether the end of the result set has been reached.
079  private final AtomicBoolean searchDone;
080
081  // The number of items that can be read immediately without blocking.
082  private final AtomicInteger count;
083
084  // The set of controls for the last result element returned.
085  private final AtomicReference<Control[]> lastControls;
086
087  // The next object to be returned.
088  private final AtomicReference<Object> nextResult;
089
090  // The search result done message for the search.
091  private final AtomicReference<SearchResult> searchResult;
092
093  // The maximum length of time in milliseconds to wait for a response.
094  private final long maxWaitTime;
095
096  // The queue used to hold results.
097  private final LinkedBlockingQueue<Object> resultQueue;
098
099
100
101  /**
102   * Creates a new LDAP search results object.
103   */
104  public LDAPSearchResults()
105  {
106    this(0L);
107  }
108
109
110
111  /**
112   * Creates a new LDAP search results object with the specified maximum wait
113   * time.
114   *
115   * @param  maxWaitTime  The maximum wait time in milliseconds.
116   */
117  public LDAPSearchResults(final long maxWaitTime)
118  {
119    this.maxWaitTime = maxWaitTime;
120
121    asyncRequestID = null;
122    searchAbandoned = new AtomicBoolean(false);
123    searchDone      = new AtomicBoolean(false);
124    count           = new AtomicInteger(0);
125    lastControls    = new AtomicReference<>();
126    nextResult      = new AtomicReference<>();
127    searchResult    = new AtomicReference<>();
128    resultQueue     = new LinkedBlockingQueue<>(50);
129  }
130
131
132
133  /**
134   * Indicates that this search request has been abandoned.
135   */
136  void setAbandoned()
137  {
138    searchAbandoned.set(true);
139  }
140
141
142
143  /**
144   * Retrieves the asynchronous request ID for the associates search operation.
145   *
146   * @return  The asynchronous request ID for the associates search operation.
147   */
148  AsyncRequestID getAsyncRequestID()
149  {
150    return asyncRequestID;
151  }
152
153
154
155  /**
156   * Sets the asynchronous request ID for the associated search operation.
157   *
158   * @param  asyncRequestID  The asynchronous request ID for the associated
159   *                         search operation.
160   */
161  void setAsyncRequestID(final AsyncRequestID asyncRequestID)
162  {
163    this.asyncRequestID = asyncRequestID;
164  }
165
166
167
168  /**
169   * Retrieves the next object returned from the server, if possible.  When this
170   * method returns, then the {@code nextResult} reference will also contain the
171   * object that was returned.
172   *
173   * @return  The next object returned from the server, or {@code null} if there
174   *          are no more objects to return.
175   */
176  private Object nextObject()
177  {
178    Object o = nextResult.get();
179    if (o != null)
180    {
181      return o;
182    }
183
184    o = resultQueue.poll();
185    if (o != null)
186    {
187      nextResult.set(o);
188      return o;
189    }
190
191    if (searchDone.get() || searchAbandoned.get())
192    {
193      return null;
194    }
195
196    try
197    {
198      final long stopWaitTime;
199      if (maxWaitTime > 0L)
200      {
201        stopWaitTime = System.currentTimeMillis() + maxWaitTime;
202      }
203      else
204      {
205        stopWaitTime = Long.MAX_VALUE;
206      }
207
208      while ((! searchAbandoned.get()) &&
209             (System.currentTimeMillis() < stopWaitTime))
210      {
211        o = resultQueue.poll(100L, TimeUnit.MILLISECONDS);
212        if (o != null)
213        {
214          break;
215        }
216      }
217
218      if (o == null)
219      {
220        if (searchAbandoned.get())
221        {
222          o = new SearchResult(-1, ResultCode.USER_CANCELED, null, null, null,
223               0, 0, null);
224          count.incrementAndGet();
225        }
226        else
227        {
228          o = new SearchResult(-1, ResultCode.TIMEOUT, null, null, null, 0, 0,
229               null);
230          count.incrementAndGet();
231        }
232      }
233    }
234    catch (final Exception e)
235    {
236      Debug.debugException(e);
237
238      if (e instanceof InterruptedException)
239      {
240        Thread.currentThread().interrupt();
241      }
242
243      o = new SearchResult(-1, ResultCode.USER_CANCELED, null, null, null, 0, 0,
244           null);
245      count.incrementAndGet();
246    }
247
248    nextResult.set(o);
249    return o;
250  }
251
252
253
254  /**
255   * Indicates whether there are any more search results to return.
256   *
257   * @return  {@code true} if there are more search results to return, or
258   *          {@code false} if not.
259   */
260  @Override()
261  public boolean hasMoreElements()
262  {
263    final Object o = nextObject();
264    if (o == null)
265    {
266      return false;
267    }
268
269    if (o instanceof SearchResult)
270    {
271      final SearchResult r = (SearchResult) o;
272      if (r.getResultCode().equals(ResultCode.SUCCESS))
273      {
274        lastControls.set(r.getResponseControls());
275        searchDone.set(true);
276        nextResult.set(null);
277        return false;
278      }
279    }
280
281    return true;
282  }
283
284
285
286  /**
287   * Retrieves the next element in the set of search results.
288   *
289   * @return  The next element in the set of search results.
290   *
291   * @throws  NoSuchElementException  If there are no more results.
292   */
293  @Override()
294  public Object nextElement()
295         throws NoSuchElementException
296  {
297    final Object o = nextObject();
298    if (o == null)
299    {
300      throw new NoSuchElementException();
301    }
302
303    nextResult.set(null);
304    count.decrementAndGet();
305
306    if (o instanceof SearchResultEntry)
307    {
308      final SearchResultEntry e = (SearchResultEntry) o;
309      lastControls.set(e.getControls());
310      return new LDAPEntry(e);
311    }
312    else if (o instanceof SearchResultReference)
313    {
314      final SearchResultReference r = (SearchResultReference) o;
315      lastControls.set(r.getControls());
316      return new LDAPReferralException(r);
317    }
318    else
319    {
320      final SearchResult r = (SearchResult) o;
321      searchDone.set(true);
322      nextResult.set(null);
323      lastControls.set(r.getResponseControls());
324      return new LDAPException(r.getDiagnosticMessage(),
325           r.getResultCode().intValue(), r.getDiagnosticMessage(),
326           r.getMatchedDN());
327    }
328  }
329
330
331
332  /**
333   * Retrieves the next entry from the set of search results.
334   *
335   * @return  The next entry from the set of search results.
336   *
337   * @throws  LDAPException  If there are no more elements to return, or if
338   *                         the next element in the set of results is not an
339   *                         entry.
340   */
341  public LDAPEntry next()
342         throws LDAPException
343  {
344    if (! hasMoreElements())
345    {
346      throw new LDAPException(null, ResultCode.NO_RESULTS_RETURNED_INT_VALUE);
347    }
348
349    final Object o = nextElement();
350    if (o instanceof LDAPEntry)
351    {
352      return (LDAPEntry) o;
353    }
354
355    throw (LDAPException) o;
356  }
357
358
359
360  /**
361   * Retrieves the number of results that are available for immediate
362   * processing.
363   *
364   * @return  The number of results that are available for immediate processing.
365   */
366  public int getCount()
367  {
368    return count.get();
369  }
370
371
372
373  /**
374   * Retrieves the response controls for the last result element returned, or
375   * for the search itself if the search has completed.
376   *
377   * @return  The response controls for the last result element returned, or
378   *          {@code null} if no elements have yet been returned or if the last
379   *          element did not include any controls.
380   */
381  public LDAPControl[] getResponseControls()
382  {
383    final Control[] controls = lastControls.get();
384    if ((controls == null) || (controls.length == 0))
385    {
386      return null;
387    }
388
389    return LDAPControl.toLDAPControls(controls);
390  }
391
392
393
394  /**
395   * {@inheritDoc}
396   */
397  @InternalUseOnly()
398  @Override()
399  public void searchEntryReturned(final SearchResultEntry searchEntry)
400  {
401    if (searchDone.get())
402    {
403      return;
404    }
405
406    try
407    {
408      resultQueue.put(searchEntry);
409      count.incrementAndGet();
410    }
411    catch (final Exception e)
412    {
413      // This should never happen.
414      Debug.debugException(e);
415
416      if (e instanceof InterruptedException)
417      {
418        Thread.currentThread().interrupt();
419      }
420
421      searchDone.set(true);
422    }
423  }
424
425
426
427  /**
428   * {@inheritDoc}
429   */
430  @InternalUseOnly()
431  @Override()
432  public void searchReferenceReturned(
433                   final SearchResultReference searchReference)
434  {
435    if (searchDone.get())
436    {
437      return;
438    }
439
440    try
441    {
442      resultQueue.put(searchReference);
443      count.incrementAndGet();
444    }
445    catch (final Exception e)
446    {
447      // This should never happen.
448      Debug.debugException(e);
449
450      if (e instanceof InterruptedException)
451      {
452        Thread.currentThread().interrupt();
453      }
454
455      searchDone.set(true);
456    }
457  }
458
459
460
461  /**
462   * Indicates that the provided search result has been received in response to
463   * an asynchronous search operation.  Note that automatic referral following
464   * is not supported for asynchronous operations, so it is possible that this
465   * result could include a referral.
466   *
467   * @param  requestID     The async request ID of the request for which the
468   *                       response was received.
469   * @param  searchResult  The search result that has been received.
470   */
471  @InternalUseOnly()
472  @Override()
473  public void searchResultReceived(final AsyncRequestID requestID,
474                                   final SearchResult searchResult)
475  {
476    if (searchDone.get())
477    {
478      return;
479    }
480
481    try
482    {
483      resultQueue.put(searchResult);
484      if (! searchResult.getResultCode().equals(ResultCode.SUCCESS))
485      {
486        count.incrementAndGet();
487      }
488    }
489    catch (final Exception e)
490    {
491      // This should never happen.
492      Debug.debugException(e);
493
494      if (e instanceof InterruptedException)
495      {
496        Thread.currentThread().interrupt();
497      }
498
499      searchDone.set(true);
500    }
501  }
502}