Wednesday, 8 June 2016

JNDI: Pagination support

Usually directory servers don’t return all the results at a time. Every directory server has a maximum size limit on search query. For example, Active directory can able to return 1000 objects (maximum) at a time. If you had 10000 entries, to get that you need to apply pagination. Following post, explains you how to apply pagination.

How can I configure page size?
It depends on resources like load on directory server, bandwidth etc.,

Directory server uses a cookie (similar to the HTTP session cookie) to maintain the state of the search requests in order to track the results being sent to the client.

JNDI provides following classes to support pagination.
javax.naming.ldap.PagedResultsControl
javax.naming.ldap.PagedResultsResponseControl

Following program gets you all the distinguished names from directory server using pagination. Following are the steps in brief.

Step 1: Get InitialLdapContext object.

I had written 'DirectoryUtil' class to get InitialLdapContext object. You need to update PROVIDER_URL, adminDN, and adminPassword details here.
InitialLdapContext ctx = (InitialLdapContext) DirectoryUtil.getContext();

Step 2: Initialize PagedResultsControl object and set it to setRequestControls method of InitialLdapContext instance. By using PagedResultsControl, we can request the results of a search operation be returned by the LDAP server in batches of a specified size.

ctx.setRequestControls(new Control[] { new PagedResultsControl(pageSize, Control.NONCRITICAL) });


Step 3: Initialize the byte array. It is used to keep track whether directory server has more results to return (or) not. In our case I am using a byte array cookie[], Directory server return cookie as null, if it don't have more results to return.
byte[] cookie = null;

Step 4: Perform the search using baseDN, search filter, search control like below.
NamingEnumeration<SearchResult> results = ctx.search(baseDn, searchFilter, searcCon);


Step 5: Following snippet is used to get all the distinguished names.
/* get the distinguished names for all search results */
while (results != null && results.hasMore()) {
 SearchResult res = results.next();
 String distinguishedName = res.getNameInNamespace();
 distinguishedNames.add(distinguishedName);
}


Step 6: Now we need to examine the paged results control response.
// Examine the paged results control response
 Control[] controls = ctx.getResponseControls();if(controls!=null)
 {
  for (int i = 0; i < controls.length; i++) {
   if (controls[i] instanceof PagedResultsResponseControl) {
    PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
    total = prrc.getResultSize();
    if (total != 0) {
     System.out
       .println("***************** END-OF-PAGE " + "(total : " + total + ") *****************\n");
    } else {
     System.out.println("***************** END-OF-PAGE " + "(total: unknown) ***************\n");
    }
    cookie = prrc.getCookie();
   }
  }
 }else
 {
 System.out.println("No controls were sent from the server");
}

Step 7: Repeat the steps 4, 5, and 6 until cookie is not equal to null.

Following is the complete working application.


DirectoryUtil.java
import java.util.Properties;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.ldap.InitialLdapContext;

public class DirectoryUtil {
 private final static String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
 private final static String PROVIDER_URL = "ldap://localhost:3268";
 private static final Properties properties = new Properties();
 private static String adminDN = "CN=admin,CN=Users,DC=example,DC=com";
 private static String adminPassword = "password";

 static {
  initProperties();
 }

 private static void initProperties() {
  properties.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
  properties.put(Context.PROVIDER_URL, PROVIDER_URL);
  properties.put("com.sun.jndi.ldap.connect.pool", "true");
  properties.put(Context.SECURITY_PRINCIPAL, adminDN);
  properties.put(Context.SECURITY_CREDENTIALS, adminPassword);
 }

 public static DirContext getContext() throws NamingException {
  DirContext context = new InitialLdapContext(properties, null);
  return context;
 }
}


Test.java
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

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

public class Test {
 public static Optional<List<String>> getDistinguishedNames(DirContext context, String baseDn, String searchFilter,
   int pageSize) {
  if (Objects.isNull(context)) {
   return Optional.empty();
  }

  if (Objects.isNull(baseDn)) {
   return Optional.empty();
  }

  if (Objects.isNull(searchFilter)) {
   return Optional.empty();
  }

  try {
   /* Define SearchControls scope to SUBTREE Level */
   SearchControls searcCon = new SearchControls();
   searcCon.setSearchScope(SearchControls.SUBTREE_SCOPE);

   List<String> distinguishedNames = new ArrayList<>();

   InitialLdapContext ctx = (InitialLdapContext) context;
   ctx.setRequestControls(new Control[] { new PagedResultsControl(pageSize, Control.NONCRITICAL) });

   byte[] cookie = null;
   int total;

   do {

    /* perform the search */
    NamingEnumeration<SearchResult> results = ctx.search(baseDn, searchFilter, searcCon);

    /* get the distinguished names for all search results */
    while (results != null && results.hasMore()) {
     SearchResult res = results.next();
     String distinguishedName = res.getNameInNamespace();
     System.out.println(distinguishedName);
     distinguishedNames.add(distinguishedName);
    }

    // Examine the paged results control response
    Control[] controls = ctx.getResponseControls();
    if (controls != null) {
     for (int i = 0; i < controls.length; i++) {
      if (controls[i] instanceof PagedResultsResponseControl) {
       PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
       total = prrc.getResultSize();
       if (total != 0) {
        System.out.println("***************** END-OF-PAGE " + "(total : " + total
          + ") *****************\n");
       } else {
        System.out.println(
          "***************** END-OF-PAGE " + "(total: unknown) ***************\n");
       }
       cookie = prrc.getCookie();
      }
     }
    } else {
     System.out.println("No controls were sent from the server");
    }
    // Re-activate paged results
    ctx.setRequestControls(new Control[] { new PagedResultsControl(pageSize, cookie, Control.CRITICAL) });

   } while (cookie != null);
   return Optional.of(distinguishedNames);

  } catch (NamingException | IOException e) {
   e.printStackTrace();
   return Optional.empty();
  }

 }

 public static void main(String args[]) throws NamingException, IOException {
  getDistinguishedNames(DirectoryUtil.getContext(), "DC=example,DC=com", "cn=*", 5);
 }
}

I am using page size as 5.

Debugging
a. If you got error like ‘javax.naming.PartialResultException: Unprocessed Continuation Reference(s); remaining name 'dc=example,dc=com'’.

Following solution may work for you.
a.   If you were using the port 389 change it to 3268
b.   If you were using the port 636 change it to 3269

A GC (global catalog) server returns referrals on 389 to refer to the greater AD "forest", but acts like a regular LDAP server on 3268 (and 3269 for LDAPS)
It worked for me.

Reference
https://docs.oracle.com/javase/tutorial/jndi/newstuff/paged-results.html








Previous                                                 Next                                                 Home

No comments:

Post a Comment