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