/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.async;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import org.ldaptive.Connection;
import org.ldaptive.LdapException;
import org.ldaptive.Response;
import org.ldaptive.SearchEntry;
import org.ldaptive.SearchReference;
import org.ldaptive.SearchRequest;
import org.ldaptive.SearchResult;
import org.ldaptive.async.handler.ExceptionHandler;
import org.ldaptive.handler.HandlerResult;
import org.ldaptive.intermediate.IntermediateResponse;
import org.ldaptive.provider.SearchItem;
import org.ldaptive.provider.SearchListener;
/**
* Executes an asynchronous ldap search operation.
*
* @author Middleware Services
*/
public class AsyncSearchOperation extends AbstractAsyncOperation<SearchRequest, SearchResult>
{
/** Cached thread executor to submit async operations to. */
private final ExecutorService executorService = Executors.newCachedThreadPool();
/** Whether the listener should spawn a new thread to process each result. */
private boolean useMultiThreadedListener;
/**
* Creates a new async search operation.
*
* @param conn connection
*/
public AsyncSearchOperation(final Connection conn)
{
super(conn);
}
/**
* Returns whether the listener should spawn a new thread to process each result.
*
* @return whether the listener is multi-threaded
*/
public boolean getUseMultiThreadedListener()
{
return useMultiThreadedListener;
}
/**
* Sets whether the listener should spawn a new thread to process each result.
*
* @param b make the listener multi-threaded
*/
public void setUseMultiThreadedListener(final boolean b)
{
useMultiThreadedListener = b;
}
@Override
public FutureResponse<SearchResult> execute(final SearchRequest request)
throws LdapException
{
final Future<Response<SearchResult>> future = executorService.submit(
() -> {
final ExceptionHandler handler = getExceptionHandler();
try {
return AsyncSearchOperation.super.execute(request);
} catch (LdapException | RuntimeException e) {
if (handler != null) {
handler.handle(getConnection(), request, e);
}
throw e;
}
});
return new FutureResponse<>(future);
}
@Override
protected Response<SearchResult> invoke(final SearchRequest request)
throws LdapException
{
final AsyncSearchListener listener = new AsyncSearchListener(request);
getConnection().getProviderConnection().searchAsync(request, listener);
try {
return listener.getResponse();
} catch (InterruptedException e) {
throw new LdapException("Asynchronous search interrupted", e);
}
}
/** Invokes {@link ExecutorService#shutdown()} on the underlying executor service. */
public void shutdown()
{
executorService.shutdown();
}
@Override
protected void finalize()
throws Throwable
{
try {
shutdown();
} finally {
super.finalize();
}
}
/** Async search listener used to build a search result and invoke search request handlers. */
protected class AsyncSearchListener implements SearchListener
{
/** Containing request handlers. */
private final SearchRequest searchRequest;
/** To build as results arrive. */
private final SearchResult searchResult;
/** Wait for the response to arrive. */
private final Semaphore responseLock = new Semaphore(0);
/** To return when a response is received or the operation is aborted. */
private Response<SearchResult> searchResponse;
/** Thrown by the async search operation. */
private LdapException searchException;
/**
* Creates a new async search listener.
*
* @param request ldap search request
*/
public AsyncSearchListener(final SearchRequest request)
{
searchRequest = request;
searchResult = new SearchResult(searchRequest.getSortBehavior());
}
@Override
public void asyncRequestReceived(final AsyncRequest request)
{
logger.trace("received async request={}", request);
if (useMultiThreadedListener) {
executorService.submit(
() -> {
try {
processAsyncRequest(request);
} catch (LdapException e) {
logger.warn("Handler exception ignored", e);
}
return null;
});
} else {
try {
processAsyncRequest(request);
} catch (LdapException e) {
logger.warn("Handler exception ignored", e);
}
}
}
@Override
public void searchItemReceived(final SearchItem item)
{
logger.trace("received search item={}", item);
if (useMultiThreadedListener) {
executorService.submit(
() -> {
try {
processSearchItem(item);
} catch (LdapException e) {
logger.warn("Handler exception ignored", e);
}
return null;
});
} else {
try {
processSearchItem(item);
} catch (LdapException e) {
logger.warn("Handler exception ignored", e);
}
}
}
@Override
public void responseReceived(final Response<Void> response)
{
searchResponse = new Response<>(
searchResult,
response.getResultCode(),
response.getMessage(),
response.getMatchedDn(),
response.getControls(),
response.getReferralURLs(),
response.getMessageId());
responseLock.release();
}
/**
* Returns the response data associated with this search, blocking until a response is available.
*
* @return response data
*
* @throws InterruptedException if this thread is interrupted before a response is received
* @throws LdapException if the async search encountered an error
*/
public Response<SearchResult> getResponse()
throws InterruptedException, LdapException
{
responseLock.acquire();
if (searchException != null) {
throw searchException;
}
return searchResponse;
}
@Override
public void exceptionReceived(final Exception exception)
{
logger.trace("received exception={}", exception);
if (exception instanceof LdapException) {
searchException = (LdapException) exception;
} else {
searchException = new LdapException(exception);
}
responseLock.release();
}
/**
* Invokes the handlers for the supplied async request. Calls {@link #responseReceived(Response)} if a handler
* aborts the operation.
*
* @param request to handle
*
* @throws LdapException if a handler throws
*/
protected void processAsyncRequest(final AsyncRequest request)
throws LdapException
{
logger.trace("processing async request={}", request);
final HandlerResult<AsyncRequest> hr = executeHandlers(getAsyncRequestHandlers(), searchRequest, request);
if (hr.getAbort()) {
logger.debug("Aborting search on async request=%s", request);
responseReceived(new Response<>(null, null));
}
}
/**
* Invokes the handlers for the supplied search item. Calls {@link #responseReceived(Response)} if a handler aborts
* the operation.
*
* @param item to handle
*
* @throws LdapException if a handler throws
*/
protected void processSearchItem(final SearchItem item)
throws LdapException
{
logger.trace("processing search item={}", item);
if (item.isSearchEntry()) {
final SearchEntry se = item.getSearchEntry();
if (se != null) {
final HandlerResult<SearchEntry> hr = executeHandlers(
searchRequest.getSearchEntryHandlers(),
searchRequest,
se);
if (hr.getResult() != null) {
searchResult.addEntry(hr.getResult());
}
if (hr.getAbort()) {
logger.debug("Aborting search on entry=%s", se);
responseReceived(new Response<>(null, null));
}
}
} else if (item.isSearchReference()) {
final SearchReference sr = item.getSearchReference();
if (sr != null) {
final HandlerResult<SearchReference> hr = executeHandlers(
searchRequest.getSearchReferenceHandlers(),
searchRequest,
sr);
if (hr.getResult() != null) {
searchResult.addReference(hr.getResult());
}
if (hr.getAbort()) {
logger.debug("Aborting search on reference=%s", sr);
responseReceived(new Response<>(null, null));
}
}
} else if (item.isIntermediateResponse()) {
final IntermediateResponse ir = item.getIntermediateResponse();
if (ir != null) {
final HandlerResult<IntermediateResponse> hr = executeHandlers(
searchRequest.getIntermediateResponseHandlers(),
searchRequest,
ir);
if (hr.getAbort()) {
logger.debug("Aborting search on intermediate response=%s", ir);
responseReceived(new Response<>(null, null));
}
}
}
}
}
}