001 /*
002 * Copyright 2009-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2009-2017 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, Filter.createEqualityFilter("objectClass", "person"));
067 * LDAPEntrySource entrySource = new LDAPEntrySource(connection,
068 * searchRequest, false);
069 *
070 * int entriesRead = 0;
071 * int referencesRead = 0;
072 * int exceptionsCaught = 0;
073 * try
074 * {
075 * while (true)
076 * {
077 * try
078 * {
079 * Entry entry = entrySource.nextEntry();
080 * if (entry == null)
081 * {
082 * // There are no more entries to be read.
083 * break;
084 * }
085 * else
086 * {
087 * // Do something with the entry here.
088 * entriesRead++;
089 * }
090 * }
091 * catch (SearchResultReferenceEntrySourceException e)
092 * {
093 * // The directory server returned a search result reference.
094 * SearchResultReference searchReference = e.getSearchReference();
095 * referencesRead++;
096 * }
097 * catch (EntrySourceException e)
098 * {
099 * // Some kind of problem was encountered (e.g., the connection is no
100 * // longer valid). See if we can continue reading entries.
101 * exceptionsCaught++;
102 * if (! e.mayContinueReading())
103 * {
104 * break;
105 * }
106 * }
107 * }
108 * }
109 * finally
110 * {
111 * entrySource.close();
112 * }
113 * </PRE>
114 */
115 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
116 public final class LDAPEntrySource
117 extends EntrySource
118 implements AsyncSearchResultListener
119 {
120 /**
121 * The bogus entry that will be used to signify the end of the results.
122 */
123 private static final String END_OF_RESULTS = "END OF RESULTS";
124
125
126
127 /**
128 * The serial version UID for this serializable class.
129 */
130 private static final long serialVersionUID = 1080386705549149135L;
131
132
133
134 // The request ID associated with the asynchronous search.
135 private final AsyncRequestID asyncRequestID;
136
137 // Indicates whether this entry source has been closed.
138 private final AtomicBoolean closed;
139
140 // The search result for the search operation.
141 private final AtomicReference<SearchResult> searchResult;
142
143 // Indicates whether to close the connection when this entry source is closed.
144 private final boolean closeConnection;
145
146 // The connection that will be used to read the entries.
147 private final LDAPConnection connection;
148
149 // The queue from which entries will be read.
150 private final LinkedBlockingQueue<Object> queue;
151
152
153
154 /**
155 * Creates a new LDAP entry source with the provided information.
156 *
157 * @param connection The connection to the directory server from which
158 * the entries will be read. It must not be
159 * {@code null}.
160 * @param searchRequest The search request that will be used to identify
161 * which entries should be returned. It must not be
162 * {@code null}, and it must not be configured with a
163 * {@link SearchResultListener}.
164 * @param closeConnection Indicates whether the provided connection should
165 * be closed whenever all of the entries have been
166 * read, or if the {@link #close} method is called.
167 *
168 * @throws LDAPException If there is a problem with the provided search
169 * request or when trying to communicate with the
170 * directory server over the provided connection.
171 */
172 public LDAPEntrySource(final LDAPConnection connection,
173 final SearchRequest searchRequest,
174 final boolean closeConnection)
175 throws LDAPException
176 {
177 this(connection, searchRequest, closeConnection, 100);
178 }
179
180
181
182 /**
183 * Creates a new LDAP entry source with the provided information.
184 *
185 * @param connection The connection to the directory server from which
186 * the entries will be read. It must not be
187 * {@code null}.
188 * @param searchRequest The search request that will be used to identify
189 * which entries should be returned. It must not be
190 * {@code null}, and it must not be configured with a
191 * {@link SearchResultListener}.
192 * @param closeConnection Indicates whether the provided connection should
193 * be closed whenever all of the entries have been
194 * read, or if the {@link #close} method is called.
195 * @param queueSize The size of the internal queue used to hold search
196 * result entries until they can be consumed by the
197 * {@link #nextEntry} method. The value must be
198 * greater than zero.
199 *
200 * @throws LDAPException If there is a problem with the provided search
201 * request or when trying to communicate with the
202 * directory server over the provided connection.
203 */
204 public LDAPEntrySource(final LDAPConnection connection,
205 final SearchRequest searchRequest,
206 final boolean closeConnection,
207 final int queueSize)
208 throws LDAPException
209 {
210 ensureNotNull(connection, searchRequest);
211 ensureTrue(queueSize > 0,
212 "LDAPEntrySource.queueSize must be greater than 0.");
213
214 this.connection = connection;
215 this.closeConnection = closeConnection;
216
217 if (searchRequest.getSearchResultListener() != null)
218 {
219 throw new LDAPException(ResultCode.PARAM_ERROR,
220 ERR_LDAP_ENTRY_SOURCE_REQUEST_HAS_LISTENER.get());
221 }
222
223 closed = new AtomicBoolean(false);
224 searchResult = new AtomicReference<SearchResult>();
225 queue = new LinkedBlockingQueue<Object>(queueSize);
226
227 final SearchRequest r = new SearchRequest(this, searchRequest.getControls(),
228 searchRequest.getBaseDN(), searchRequest.getScope(),
229 searchRequest.getDereferencePolicy(), searchRequest.getSizeLimit(),
230 searchRequest.getTimeLimitSeconds(), searchRequest.typesOnly(),
231 searchRequest.getFilter(), searchRequest.getAttributes());
232 asyncRequestID = connection.asyncSearch(r);
233 }
234
235
236
237 /**
238 * {@inheritDoc}
239 */
240 @Override()
241 public Entry nextEntry()
242 throws EntrySourceException
243 {
244 while (true)
245 {
246 if (closed.get() && queue.isEmpty())
247 {
248 return null;
249 }
250
251 final Object o;
252 try
253 {
254 o = queue.poll(10L, TimeUnit.MILLISECONDS);
255 }
256 catch (InterruptedException ie)
257 {
258 debugException(ie);
259 Thread.currentThread().interrupt();
260 throw new EntrySourceException(true,
261 ERR_LDAP_ENTRY_SOURCE_NEXT_ENTRY_INTERRUPTED.get(), ie);
262 }
263
264 if (o != null)
265 {
266 if (o == END_OF_RESULTS)
267 {
268 return null;
269 }
270 else if (o instanceof Entry)
271 {
272 return (Entry) o;
273 }
274 else
275 {
276 throw (EntrySourceException) o;
277 }
278 }
279 }
280 }
281
282
283
284 /**
285 * {@inheritDoc}
286 */
287 @Override()
288 public void close()
289 {
290 closeInternal(true);
291 }
292
293
294
295 /**
296 * Closes this LDAP entry source.
297 *
298 * @param abandon Indicates whether to attempt to abandon the search.
299 */
300 private void closeInternal(final boolean abandon)
301 {
302 addToQueue(END_OF_RESULTS);
303
304 if (closed.compareAndSet(false, true))
305 {
306 if (abandon)
307 {
308 try
309 {
310 connection.abandon(asyncRequestID);
311 }
312 catch (Exception e)
313 {
314 debugException(e);
315 }
316 }
317
318 if (closeConnection)
319 {
320 connection.close();
321 }
322 }
323 }
324
325
326
327 /**
328 * Retrieves the search result for the search operation, if available. It
329 * will not be available until the search has completed (as indicated by a
330 * {@code null} return value from the {@link #nextEntry} method).
331 *
332 * @return The search result for the search operation, or {@code null} if it
333 * is not available (e.g., because the search has not yet completed).
334 */
335 public SearchResult getSearchResult()
336 {
337 return searchResult.get();
338 }
339
340
341
342 /**
343 * {@inheritDoc} This is intended for internal use only and should not be
344 * called by anything outside of the LDAP SDK itself.
345 */
346 @InternalUseOnly()
347 public void searchEntryReturned(final SearchResultEntry searchEntry)
348 {
349 addToQueue(searchEntry);
350 }
351
352
353
354 /**
355 * {@inheritDoc} This is intended for internal use only and should not be
356 * called by anything outside of the LDAP SDK itself.
357 */
358 @InternalUseOnly()
359 public void searchReferenceReturned(
360 final SearchResultReference searchReference)
361 {
362 addToQueue(new SearchResultReferenceEntrySourceException(searchReference));
363 }
364
365
366
367 /**
368 * {@inheritDoc} This is intended for internal use only and should not be
369 * called by anything outside of the LDAP SDK itself.
370 */
371 @InternalUseOnly()
372 public void searchResultReceived(final AsyncRequestID requestID,
373 final SearchResult searchResult)
374 {
375 this.searchResult.set(searchResult);
376
377 if (! searchResult.getResultCode().equals(ResultCode.SUCCESS))
378 {
379 addToQueue(new EntrySourceException(false,
380 new LDAPSearchException(searchResult)));
381 }
382
383 closeInternal(false);
384 }
385
386
387
388 /**
389 * Adds the provided object to the queue, waiting as long as needed until it
390 * has been added.
391 *
392 * @param o The object to be added. It must not be {@code null}.
393 */
394 private void addToQueue(final Object o)
395 {
396 while (true)
397 {
398 if (closed.get())
399 {
400 return;
401 }
402
403 try
404 {
405 if (queue.offer(o, 100L, TimeUnit.MILLISECONDS))
406 {
407 return;
408 }
409 }
410 catch (InterruptedException ie)
411 {
412 debugException(ie);
413 }
414 }
415 }
416 }