2.3 - Searching (…)

Searching is the most important operation in LDAP. It has to be fast, very fast. On the other hand, as the server does the processing while looking for entries, the client must provide information to get accurate results.

The idea is to define a search API which is easy to use in the simplest cases, but provides all the capability to send complex search requests.

Let’s first look at a simple search. To process a search we need a starting point in the tree, a filter, and a scope. Here’s an example:

    @Test
    public void testSearch() throws Exception
    {
        EntryCursor cursor = connection.search( "ou=system", "(objectclass=*)", SearchScope.ONELEVEL );

        try
        {
            for ( Entry entry : cursor )
            {
                assertNotNull( entry );
                System.out.println( entry );
            }
        }
        finally
        {
            cursor.close();
        }
    }

In this example, the connection has been previously created. We search for all entries starting at ou=system along with its children, which have an ObjectClass attribute (all the entries have such an attribute, so we should get back all the entries). The scope (ONELEVEL) searches one level under the starting base.

A cursor of entries is returned, which can be iterated over. Every call to the getEntry() method returns the next entry in the LDAP result set.

That’s pretty much it!

But this is not really enough, there are many possible options.

Note Don’t forget to close the cursor, otherwise the associated data remains in memory forever (causing a memory leak)! Best practice is to use try-with-resources statement, like in this better example of code:

    @Test
    public void testSearch() throws Exception
    {
        try ( EntryCursor cursor = connection.search( "ou=system", "(objectclass=*)", SearchScope.ONELEVEL ) )
        {

            for ( Entry entry : cursor )
            {
                assertNotNull( entry );
                System.out.println( entry );
            }
        }
    }

Searching using a DN

In the previous sample, we used a String to define the starting point of the search. Sometimes it’s convenient to start a search with a DN (i.e. because you got the DN from another operation). In this case, no need to transform the DN into a String before doing your search, simply use the DN!

    @Test
    public void testSearchWithDn() throws Exception
    {
        Dn systemDn = new Dn( "ou=system" );

        try ( EntryCursor cursor = connection.search( systemDn, "(objectclass=*)", SearchScope.ONELEVEL ) )
        {
            for ( Entry entry : cursor )
            {
                assertNotNull( entry );
                System.out.println( entry );
            }
        }
    }

This is it!

Scope

There are three different different scopes you can use to search for data:

  • SearchScope.OBJECT : return the entry for a given DN, if it exists. Note that you could use LdapConnection.lookup if the filter is irrelevent.
  • SearchScope.ONELEVEL : return all elements below the current DN, but not the element associated with the DN.
  • SearchScope.SUBLEVEL : return all the elements starting from the given DN, including the element associated with the DN, whatever the depth of the tree.

Filter

The filter is used to define the elements that are targeted. There are various possibilities to construct a filter, using one or more connectors, and one or more expression nodes.

Connectors use a prefix notation, followed by as many expression nodes as necessary, like in (& (node1) (node2) … (nodeN))

Expression nodes are always contained within parenthesis, with a left part - the attributeType, and a right part - the value -.

Here is the list of possible connectors:

  • & : n-ary AND connector, all the nodes must evaluate to TRUE
  • | : n-ary OR connector, at least one of the node must evaluate to true
  • ! : 1-ary NOT connector, the node must evaluate to false

And here is the list of possible expression nodes, assuming that an expression node:

  • = Equality expression node : the selected entry matches the right part
  • =* Presence expression node : the entry has the Attribute on the left side
  • >= Superior expression node : the entry should be superior to the right part
  • <= Inferior expression node : the entry should be superior to the right part
  • =[start][*][middle][*][final] Substring expression node : the entry should match a substring

Note: As of Apache DS 2.0, we don’t support approx matches nor extensible matches.

More complex searches

Sometimes, you want to have more control over the search operation, either with the parameters in use, or the results that are returned.

A search can return other things than entries. In fact, you can get four different kinds of responses:

  • When it’s a normal entry, you will receive a SearchResultEntry
  • When it’s done, you will receive a SearchResultDone
  • When the response is a reference to another entry, you will get a SearchResultReference
  • In some cases, you may also receive an IntermediateResponse.

You may also add a Control to the search request, or request some specific AttributeType to be returned. The parameters that may be injected into a SearchRequest are as follows:

  • The base DN
  • The filter
  • The scope (one of OBJECT, ONELEVEL or SUBTREE)
  • The size limit
  • The time limit
  • The list of attributes to return
  • The alias dereferencing mode (one of DEREF_ALWAYS, DEREF_FINDING_BASE_OBJ, DEREF_IN_SEARCHING or NEVER_DEREF_ALIASES)
  • The TypesOnly flag (to get the AttributeTypes but no values)
  • The controls

In both cases, you should fill the SearchRequest message and send it to the server:

    @Test
    public void testSearchWithSearchRequest() throws Exception
    {
        // Create the SearchRequest object
        SearchRequest req = new SearchRequestImpl();
        req.setScope( SearchScope.SUBTREE );
        req.addAttributes( "*" );
        req.setTimeLimit( 0 );
        req.setBase( new Dn( "ou=system" ) );
        req.setFilter( "(cn=user1)" );

        // Process the request
        try ( SearchCursor searchCursor = connection.search( req ) ) 
        {
            while ( searchCursor.next() )
            {
                Response response = searchCursor.get();

                // process the SearchResultEntry
                if ( response instanceof SearchResultEntry )
                {
                    Entry resultEntry = ( ( SearchResultEntry ) response ).getEntry();
                    System.out.println( resultEntry );
                }
            }
        }
    }

As we can see, the response is different, we are returned a SearchCursor instance, and we must verify the response type before being able to process it.