User Tools

Site Tools


database:ldap

Wenn man LDAP Queries absetzen muss, deren Result-Set groß (oder zumindest größer als die maximal erlaubte “Zeilenzahl”) ist, muss man ziemlich altertümlich mit PagedResultsResponseControl umgehen. Folgende Klasse kapselt das Paging von LDAP Searches in einem Iterator. Wichtig dabei ist, dass man den Iterator am Ende mit close() schließt. (Selbstverständlich kann ich keinerlei Garantie geben, dass die Klasse 100% einwandfrei funktioniert - also bitte wenn, dann auf eigene Gefahr verwenden.)

package de.wellcrafted.util.ldap;

import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Properties;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;

import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * An {@link Iterator} over a search in LDAP, if requested doing a paged search.
 * 
 * @author Heinrich Goebl
 */
public class LdapSearchIterator implements Iterator {
  private static final Log LOG = LogFactory.getLog(LdapSearchIterator.class);

  private String baseDN;
  private SearchControls constraints = new SearchControls();
  private Properties environment;
  private String searchString;
  private int pageSize;
  private int pageCount;

  private NamingEnumeration<SearchResult> results;
  private boolean closed;
  private SearchResult nextResult;
  private LdapContext ctx;
  private byte[] cookie;

  /**
   * Create Iterator for LDAP search w/o internal paging.
   * 
   * @param environment properties for the {@link InitialLdapContext}.
   * @param baseDN base distinguished name
   * @param searchString the LDAP query string
   * @param constraints the constraints for the search
   */
  public LdapSearchIterator(Properties environment, String baseDN, String searchString, SearchControls constraints) {
    this(environment, baseDN, searchString, constraints, 0);
  }

  /**
   * Like {@link #LdapSearchIterator(Properties, String, String, SearchControls)} but with internal paging.
   * <p>
   * The LDAP search is realized with paging which is transparent for the user of this class.
   * 
   * @param environment properties for the {@link InitialLdapContext}.
   * @param baseDN base distinguished name
   * @param searchString the LDAP query string
   * @param constraints the constraints for the search
   * @param pageSize the size of one page (reasonable values depend on the size of the records and the maximum count of
   *          records that are delivered by the LDAP server.)
   */
  public LdapSearchIterator(Properties environment, String baseDN, String searchString, SearchControls constraints,
      int pageSize) {
    Validate.notNull(environment);
    Validate.notEmpty(baseDN);
    Validate.notEmpty(searchString);
    Validate.isTrue(pageSize >= 0, "pageSize must not be negative");

    this.environment = environment;
    this.baseDN = baseDN;
    this.searchString = searchString;
    this.constraints = constraints;
    this.pageSize = pageSize;
  }

  public void close() {
    if (closed)
      return;
    if (results != null) {
      try {
        results.close();
      } catch (NamingException ignored) {
      }
    }
    results = null;
    closeContext();
    closed = true;
  }

  public SearchResult next() {
    if (!hasNext())
      throw new NoSuchElementException();

    SearchResult returnValue = nextResult;
    nextResult = null; // mark as consumed
    return returnValue;
  }

  public boolean hasNext() {
    if (nextResult != null)
      return true;

    if (closed)
      return false;

    try {
      if (results == null) {
        if (hasMorePages())
          nextPage(); // could also be the first page
        else
          return false;
      }

      if (results.hasMoreElements()) {
        nextResult = results.next();

        // prepare for the next call to hasNext()
        if (!results.hasMoreElements())
          results = null;

        return true;
      }
      return false;

    } catch (NamingException e) {
      LOG.warn("unexpected exception while iterating LDAP search: " + e);
      closeContext();
      throw new RuntimeException("iterating LDAP search failed", e);
    }
  }

  private boolean hasMorePages() throws NamingException {
    if (results != null)
      throw new IllegalStateException("must be called after end of enumeration");

    if (ctx == null)
      return true;

    if (pageSize == 0)
      return false;

    // Examine the paged results control response (only works after results consumed!)
    Control[] controls = ctx.getResponseControls();
    if (controls != null) {
      for (Control control : controls) {
        if (control instanceof PagedResultsResponseControl) {
          PagedResultsResponseControl prrc = (PagedResultsResponseControl) control;
          cookie = prrc.getCookie();
          if (cookie != null)
            LOG.debug("found more pages");
        }
      }
    }
    return cookie != null;
  }

  private void nextPage() throws NamingException {
    ++pageCount;
    if (ctx == null)
      ctx = new InitialLdapContext(environment, null); // set Control[] later!

    if (pageSize > 0) {
      LOG.debug("nextPage() - pageCount=" + pageCount);
      ctx.setRequestControls(createPagedResultsControl(pageSize, cookie));
    }

    results = ctx.search(baseDN, searchString, constraints);
  }

  public void remove() {
    throw new UnsupportedOperationException();
  }

  private Control[] createPagedResultsControl(int pageSize, byte[] cookie) {
    try {
      return new Control[] { new PagedResultsControl(pageSize, cookie, Control.CRITICAL) };
    } catch (IOException e) {
      LOG.warn("Unexpected IOException when creating LDAP Control: " + e);
      throw new RuntimeException("creating LDAP Control failed", e);
    }
  }

  private void closeContext() {
    if (ctx != null) {
      try {
        ctx.close();
      } catch (NamingException ignored) {
        LOG.debug("NamingException when closing Context");
      }
      ctx = null;
    }
  }

}

database/ldap.txt · Last modified: 2011/01/21 22:16 by hgoebl