Chapter 6. Searching & Comparing Directory Data

Table of Contents
6.1. About Searching
6.2. Setting Search Base & Scope
6.3. Working With Search Filters
6.4. Sending a Search Request
6.5. Getting Search Results
6.6. Working With Entry Attributes
6.7. Working With LDAP URLs
6.8. Sorting Search Results
6.9. About Comparing

Traditionally directories excel at serving read requests. This chapter covers the read (search and compare) capabilities that OpenDJ LDAP Java SDK provides. The data used in examples here is available online.

6.1. About Searching

An LDAP search looks up entries based on the following parameters.

  • A filter that indicates which attribute values to match

  • A base DN that specifies where in the directory information tree to look for matches

  • A scope that defines how far to go under the base DN

  • A list of attributes to fetch for an entry when a match is found

For example, imagine you must write an application where users login using their email address and a password. After the user logs in, your application displays the user's full name so it is obvious who is logged in. Your application is supposed to go to the user directory both for authentication, and also to read user profile information. You are told the user directory stores user profile entries under base DN ou=People,dc=example,dc=com, that email addresses are stored on the standard mail attribute, and full names are store on the standard cn attribute.

You figure out how to authenticate from the chapter on authentication, in which you learn you need a bind DN and a password to do simple authentication. But how do you find the bind DN given the email? How do you get the full name?

The answer to both questions is that you do an LDAP search for the user's entry, which has the DN that you use to bind, and you have the server fetch the cn attribute in the results. Your search uses the following parameters.

  • The filter is (mail=emailAddress), where emailAddress is the email address the user provided.

  • The base DN is the one given to you, ou=People,dc=example,dc=com.

  • For the scope, you figure the user entry is somewhere under the base DN, so you opt to search the whole subtree.

  • The attribute to fetch is cn.

The following code excerpt demonstrates how this might be done in a minimal command-line program.

// Prompt for mail and password.
Console c = System.console();
if (c == null) {
    System.err.println("No console.");
    System.exit(1);
}

String mail = c.readLine("Email address: ");
char[] password = c.readPassword("Password: ");

// Search using mail address, and then bind with the DN and password.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(host,
        port);
Connection connection = null;
try {
    connection = factory.getConnection();

    // No explicit bind yet so we remain anonymous for now.
    SearchResultEntry entry = connection.searchSingleEntry(baseDN,
            SearchScope.WHOLE_SUBTREE, "(mail=" + mail + ")", "cn");
    DN bindDN = entry.getName();
    connection.bind(bindDN.toString(), password);

    String cn = entry.getAttribute("cn").firstValueAsString();
    System.out.println("Hello, " + cn + "!");
} catch (final ErrorResultException e) {
    System.err.println("Failed to bind.");
    System.exit(e.getResult().getResultCode().intValue());
    return;
} finally {
    if (connection != null) {
        connection.close();
    }
}

For a complete example in context, see SearchBind.java, one of the OpenDJ LDAP SDK examples.

6.2. Setting Search Base & Scope

Directory servers organize entries somewhat like a file system. Directory data is often depicted as an upside-down tree.

Directory data is often depicted as an upside-down tree.

In the figure shown above, entries are represented by the relevant parts of their DNs. The entry with DN dc=example,dc=com is the base entry for a suffix. Under the base entry, you see two organizational units, one for people, ou=People, the other for groups, ou=Groups. The entries for people include those of Babs Jensen, Kirsten Vaughan, and Sam Carter.

When you are searching for a person's entry somewhere under dc=example,dc=com, you can start from dc=example,dc=com, from ou=People,dc=example,dc=com, or if you have enough information to pinpoint the user entry and only want to look up another attribute value for example, then directly from the entry such as cn=Babs Jensen,ou=People,dc=example,dc=com. The DN of the entry where you choose to start the search is the base DN for the search.

When searching, you also define the scope. Scope defines what entries the server considers when checking for entries that match your search.

  • For SearchScope.BASE_OBJECT the server considers only the base entry.

    This is the scope you use if you know the full DN of the object that interests you. For example, if your base DN points to Babs Jensen's entry, cn=Babs Jensen,ou=People,dc=example,dc=com, and you want to read some of Babs's attributes, you would set scope to SearchScope.BASE_OBJECT.

  • For SearchScope.SINGLE_LEVEL the server considers all entries directly below the base entry.

    You use this scope if for example you want to discover organizational units under dc=example,dc=com, or if you want to find people's entries and you know they are immediately under ou=People,dc=example,dc=com.

  • For SearchScope.SUBORDINATES the server considers all entries below the base entry.

    This scope can be useful if you know that the base DN for your search is an entry that you do not want to match.

  • For SearchScope.WHOLE_SUBTREE (default) the server considers the base entry and all entries below.

In addition to a base DN and scope, a search request also calls for a search filter.

6.3. Working With Search Filters

When you look someone up in the telephone directory, you use the value of one attribute of a person's entry (last name), to recover the person's directory entry, which has other attributes (phone number, address). LDAP works the same way. In LDAP, search requests identify both the scope of the directory entries to consider (for example, all people or all organizations), and also the entries to retrieve based on some attribute value (for example, surname, mail address, phone number, or something else). The way you express the attribute value(s) to match is by using a search filter.

LDAP search filters define what entries actually match your request. For example, the following simple equality filter says, "Match all entries that have a surname attribute (sn) value equivalent to Jensen."

(sn=Jensen)

When you pass the directory server this filter as part of your search request, the directory server checks the entries in scope for your search to see whether they match.[7] If the directory server finds entries that match, it returns those entries as it finds them.

The example, (sn=Jensen), shows a string representation of the search filter. The OpenDJ LDAP SDK lets you express your filters as strings, or as Filter objects. In both cases, the SDK translates the strings and objects into the binary representation sent to the server over the network.

Equality is just one of the types of comparisons available in LDAP filters. Comparison operators include the following.

Table 6.1. LDAP Filter Operators
OperatorDefinitionExample
=

Equality comparison, as in (sn=Jensen).

This can also be used with substring matches. For example, to match last names starting with Jen, use the filter (sn=Jen*). Substrings are more expensive for the directory server to index. Substring searches therefore might not be permitted for many attributes.

"(cn=My App)" matches entries with common name My App.

"(sn=Jen*)" matches entries with surname starting with Jen.

<=

Less than or equal to comparison, which works alphanumerically.

"(cn<=App)" matches entries with commonName up to those starting with App (case-insensitive) in alphabetical order.

>=

Greater than or equal to comparison, which works alphanumerically.

"(uidNumber>=1151)" matches entries with uidNumber greater than 1151.

=*

Presence comparison. For example, to match all entries having a userPassword, use the filter (userPassword=*).

"(member=*)" matches entries with a member attribute.

~=

Approximate comparison, matching attribute values similar to the value you specify.

"(sn~=jansen)" matches entries with a surname that sounds similar to Jansen (Johnson, Jensen, and so forth).

[:dn][:oid]:=

Extensible match comparison.

At the end of the OID or language subtype, you further specify the matching rule as follows:

  • Add .1 for less than

  • Add .2 for less than or equal to

  • Add .3 for equal to (default)

  • Add .4 for greater than or equal to

  • Add .5 for greater than

  • Add .6 for substring

(uid:dn:=bjensen) matches entries where uid having the value bjensen is a component of the entry DN.

(lastLoginTime: 1.3.6.1.4.1.26027.1.4.5:=-13w) matches entries with a last login time more recent than 13 weeks.

You also use extensible match filters with localized values. Directory servers like OpenDJ support a variety of internationalized locales, each of which has an OID for collation order, such as 1.3.6.1.4.1.42.2.27.9.4.76.1 for French. OpenDJ also lets you use the language subtype, such as fr, instead of the OID.

"(cn:dn:=My App)" matches entries who have My App as the common name and also as the value of a DN component.

!

NOT operator, to find entries that do not match the specified filter component.

Take care to limit your search when using ! to avoid matching so many entries that the server treats your search as unindexed.

'!(objectclass=person)' matches non-person entries.

&

AND operator, to find entries that match all specified filter components.

'(&(l=Cupertino)(!(uid=bjensen)))' matches entries for users in Cupertino other than the user with ID bjensen.

|

OR operator, to find entries that match one of the specified filter components.

"|(sn=Jensen)(sn=Johnson)" matches entries with surname Jensen or surname Johnson.


When taking user input, take care to protect against users providing input that has unintended consequences. OpenDJ SDK offers several Filter methods to help you. First, you can use strongly typed construction methods such as Filter.equality().

String userInput = getUserInput();
Filter filter = Filter.equality("cn", userInput);

// Invoking filter.toString() with input of "*" results in a filter
// string "(cn=\2A)".

You can also let the SDK escape user input by using a template with Filter.format() as in the following example.

String template = "(|(cn=%s)(uid=user.%s))";
String[] userInput = getUserInput();
Filter filter = Filter.format(template, userInput[0], userInput[1]);

Finally, you can explicitly escape user input with Filter.escapeAssertionValue().

String baseDN = "ou=people,dc=example,dc=com";
String userInput = getUserInput();

// Filter.escapeAssertionValue() transforms user input of "*" to "\2A".
SearchRequest request = Requests.newSearchRequest(
        baseDN, SearchScope.WHOLE_SUBTREE,
        "(cn=" + Filter.escapeAssertionValue(userInput) + "*)", "cn", "mail");

6.4. Sending a Search Request

As shown in the following excerpt with a synchronous connection, you get a Connection to the directory server from an LDAPConnectionFactory.

final LDAPConnectionFactory factory = new LDAPConnectionFactory(host,
        port);
Connection connection = null;
try {
    connection = factory.getConnection();

    // Do something with the connection...
} catch (Exception e) {
    // Handle exceptions...
} finally {
    if (connection != null) {
        connection.close();
    }
}

The Connection gives you search() methods that either take parameters in the style of the ldapsearch command, or that take a SearchRequest object. If you are sure that the search only returns a single entry, you can read the entry with the searchSingleEntry() methods. If you have the distinguished name, you can use readEntry() directly.

For a complete example in context, see Search.java, one of the OpenDJ LDAP SDK examples.

6.5. Getting Search Results

Depending on the method you use to search, you handle results in different ways.

  • You can get a ConnectionEntryReader, and iterate over the reader to access individual search results.

    Connection connection = ...;
    ConnectionEntryReader reader = connection.search("dc=example,dc=com",
        SearchScope.WHOLE_SUBTREE, "(objectClass=person)");
    try
    {
      while (reader.hasNext())
      {
        if (reader.isEntry())
        {
          SearchResultEntry entry = reader.readEntry();
    
          // Handle entry...
        }
        else
        {
          SearchResultReference ref = reader.readReference();
    
          // Handle continuation reference...
        }
      }
    }
    catch (IOException e)
    {
      // Handle exceptions...
    }
    finally
    {
      reader.close();
    }

    For a complete example in context, see Search.java, one of the OpenDJ LDAP SDK examples.

  • You can pass in a collection of SearchResultEntrys (and optionally a collection of SearchResultReferences) to which the SDK adds the results. For this to work, you need enough memory to hold everything the search returns.

  • You can pass in a SearchResultHandler to manage results.

  • With searchSingleEntry() and readEntry(), you can get a single SearchResultEntry with methods to access the entry content.

6.6. Working With Entry Attributes

When you get an entry object, chances are you want to handle attribute values as objects. The OpenDJ LDAP SDK provides the Entry.parseAttribute() method and an AttributeParser with methods for a variety of attribute value types. You can use these methods to get attribute values as objects.

// Use Kirsten Vaughan's credentials and her entry.
String name = "uid=kvaughan,ou=People,dc=example,dc=com";
char[] password = "bribery".toCharArray();
connection.bind(name, password);

// Make sure we have a timestamp to play with.
updateEntry(connection, name, "description");

// Read Kirsten's entry.
final SearchResultEntry entry = connection.readEntry(name,
        "cn", "objectClass", "hasSubordinates", "numSubordinates",
        "isMemberOf", "modifyTimestamp");

// Get the entry DN and some attribute values as objects.
DN dn = entry.getName();

Set<String> cn = entry.parseAttribute("cn").asSetOfString("");
Set<AttributeDescription> objectClasses =
        entry.parseAttribute("objectClass").asSetOfAttributeDescription();
boolean hasChildren = entry.parseAttribute("hasSubordinates").asBoolean();
int numChildren = entry.parseAttribute("numSubordinates").asInteger(0);
Set<DN> groups = entry
        .parseAttribute("isMemberOf")
        .usingSchema(Schema.getDefaultSchema()).asSetOfDN();
Calendar timestamp = entry
        .parseAttribute("modifyTimestamp")
        .asGeneralizedTime().toCalendar();

// Do something with the objects.
// ...

For a complete example in context, see ParseAttributes.java, one of the OpenDJ LDAP SDK examples.

6.7. Working With LDAP URLs

LDAP URLs express search requests in URL form. In the directory data you can find them used as memberURL attribute values for dynamic groups, for example. The following URL from the configuration for the administrative backend lets the directory server build a dynamic group of administrator entries that are children of cn=Administrators,cn=admin data.

ldap:///cn=Administrators,cn=admin data??one?(objectclass=*)

The static method LDAPUrl.valueOf() takes an LDAP URL string and returns an LDAPUrl object. You can then use the LDAPUrl.asSearchRequest() method to get the SearchRequest that you pass to one of the search methods for the connection.

6.8. Sorting Search Results

If you want to sort search results in your client application, then make sure you have enough memory in the JVM to hold the results of the search, and use one of the search methods that lets you pass in a collection of SearchResultEntrys. After the collection is populated with the results, you can sort them.

If you are on good terms with your directory administrator, you can perhaps use a server-side sort control. The server-side sort request control asks the server to sort the results before returning them, and so is a memory intensive operation on the directory server. You set up the control using ServerSideSortRequestControl.newControl(). You get the control into your search by building a search request to pass to the search method, using SearchRequest.addControl() to attach the control before passing in the request.

If your application needs to scroll through search results a page at a time, work with your directory administrator to set up the virtual list view indexes that facilitate scrolling through results.

6.9. About Comparing

You use the LDAP compare operation to make an assertion about an attribute value on an entry. Unlike the search operation, you must know the distinguished name of the entry in advance to request a compare operation. You also specify the attribute type name and the value to compare to the values stored on the entry.

Connection has a choice of compare methods, depending on how you set up the operation.

Check the ResultCode from CompareResult.getResultCode() for ResultCode.COMPARE_TRUE or ResultCode.COMPARE_FALSE.



[7] In fact, the directory server probably checks an index first, and might not even accept search requests unless it can use indexes to match your filter rather than checking all entries in scope.