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.
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=
, where
emailAddress
)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.
Directory servers organize entries somewhat like a file system. 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.
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.
Operator | Definition | Example |
---|---|---|
= |
Equality comparison, as in This can also be used with substring matches. For example, to match
last names starting with |
|
<= |
Less than or equal to comparison, which works alphanumerically. |
|
>= |
Greater than or equal to comparison, which works alphanumerically. |
|
=* |
Presence comparison. For example, to match all entries having a
|
|
~= |
Approximate comparison, matching attribute values similar to the value you specify. |
|
[:dn][: |
Extensible match comparison. At the end of the OID or language subtype, you further specify the matching rule as follows:
|
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
|
! |
NOT operator, to find entries that do not match the specified filter component. Take care to limit your search when using |
|
& |
AND operator, to find entries that match all specified filter components. |
|
| |
OR operator, to find entries that match one of the specified filter components. |
|
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");
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.
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 SearchResultEntry
s
(and optionally a collection of SearchResultReference
s)
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.
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.
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.
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
SearchResultEntry
s. 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.
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.