/*******************************************************************************
* ADSync4J (https://github.com/zagyi/adsync4j)
*
* Copyright (c) 2013 Balazs Zagyvai
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Balazs Zagyvai
***************************************************************************** */
package org.adsync4j.unboundid;
import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.sdk.*;
import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
import org.adsync4j.api.LdapClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import static com.unboundid.ldap.sdk.controls.SimplePagedResultsControl.PAGED_RESULTS_OID;
/**
* Iterator returning a single page of {@link com.unboundid.ldap.sdk.SearchResultEntry} elements on each iteration.
* Clients are expected to build a search request that includes a {@link SimplePagedResultsControl}, and to make an initial
* call to {@link LDAPInterface#search(com.unboundid.ldap.sdk.SearchRequest)} with it. The iterator can then be constructed
* with the {@link SearchResult} returned to this initial search invocation.
* <p/>
* Subsequent searches are executed by the iterator when {@link PagingSearchIterator#next next()} is called (using the same page
* size as that of the initial search request).
*/
public class PagingSearchIterator implements Iterator<List<SearchResultEntry>> {
private final static Logger LOG = LoggerFactory.getLogger(PagingSearchIterator.class);
private final LDAPInterface _connection;
private final SearchRequest _searchRequest;
private final int _pageSize;
@Nullable
ASN1OctetString _pagingCookie = null;
List<SearchResultEntry> _firstPage;
/**
* @param connection The connection on which the search request is to be executed.
* @param searchRequest The search request containing a {@link SimplePagedResultsControl}.
* @param firstResult The result of the initial search request.
*/
public PagingSearchIterator(LDAPInterface connection, SearchRequest searchRequest, SearchResult firstResult) {
_connection = connection;
_searchRequest = searchRequest;
_pageSize = getPageSize(searchRequest);
_firstPage = firstResult.getSearchEntries();
_pagingCookie = getPagingCookieForNextIteration(firstResult);
LOG.debug("Instance created with an initial search result that indicates there will be {}more pages.",
_pagingCookie == null ? "NO " : "");
}
/**
* @param searchRequest A {@link SearchRequest} that contains the {@link SimplePagedResultsControl} control object.
* @return The page size specified in the {@link SimplePagedResultsControl} control object.
*/
private static int getPageSize(SearchRequest searchRequest) {
SimplePagedResultsControl pagingControl = (SimplePagedResultsControl) searchRequest.getControl(PAGED_RESULTS_OID);
if (pagingControl == null) {
throw new IllegalArgumentException("The search request must contain a SimplePagedResultsControl control object.");
}
return pagingControl.getSize();
}
@Override
public boolean hasNext() {
boolean isFirstPageAlreadyReturned = _firstPage == null;
if (isFirstPageAlreadyReturned) {
// the server indicates the last page by not including a paging cookie in the response (see SimplePagedResultsControl)
boolean didServerReturnAPagingCookie = _pagingCookie != null && _pagingCookie.getValueLength() > 0;
return didServerReturnAPagingCookie;
} else {
return !_firstPage.isEmpty();
}
}
@Override
public List<SearchResultEntry> next() {
if (hasNext()) {
if (_firstPage == null) {
return fetchNextPage();
} else {
return getAndReleaseFirstPage();
}
}
throw new NoSuchElementException();
}
/**
* Simply returns the reference to the first page of search results stored in {@code _firstPage},
* but also nulls out that reference in order to avoid retaining those entries in memory longer than necessary.
*
* @return The search results referenced by {@code _firstPage}.
*/
private List<SearchResultEntry> getAndReleaseFirstPage() {
List<SearchResultEntry> firstPage = _firstPage;
_firstPage = null;
return firstPage;
}
/**
* Performs a search operation to fetch the next page of results. Uses the cached paging cookie for the request,
* and updates it with the paging cookie received in the response.
*
* @return The next page of results.
*/
private List<SearchResultEntry> fetchNextPage() {
_searchRequest.replaceControl(new SimplePagedResultsControl(_pageSize, _pagingCookie));
try {
LOG.debug("Fetching subsequent result page.");
SearchResult searchResult = _connection.search(_searchRequest);
_pagingCookie = getPagingCookieForNextIteration(searchResult);
LOG.debug("Search result page received, response indicates it's {} page.",
_pagingCookie == null ? "the final" : "an intermediate");
return searchResult.getSearchEntries();
} catch (LDAPSearchException e) {
throw new LdapClientException(e);
}
}
/**
* Extracts the paging cookie from the {@link SearchResult}.
*
* @param searchResult The {@link SearchResult} to extract the paging cookie from.
* @return The paging cookie contained in the {@link SimplePagedResultsControl} of the {@link SearchResult} if any,
* {@code null} otherwise.
*/
@Nullable
/*package*/ static ASN1OctetString getPagingCookieForNextIteration(@Nonnull SearchResult searchResult) {
try {
SimplePagedResultsControl pagedCtrlResponse = SimplePagedResultsControl.get(searchResult);
ASN1OctetString pagingCookie = pagedCtrlResponse == null ? null : pagedCtrlResponse.getCookie();
return pagingCookie == null ? null :
pagingCookie.getValueLength() == 0 ? null : pagingCookie;
} catch (LDAPException e) {
throw new LdapClientException(e);
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}