UnboundID LDAP SDK for Java

Ping Identity

Product Information
Getting Started with the LDAP SDK

Using Standard Controls

The UnboundID LDAP SDK for Java provides support for a number of common controls that are either defined in RFCs, or are based on IETF Internet Drafts that are considered stable enough to be implemented by many directory server implementations.

Including Controls in Requests

All of the request objects provided by the UnboundID LDAP SDK for Java allow you to include controls in the request. You can include the controls in the constructor when creating the request object, or you can use the getControls, setControls, addControl, removeControl, and clearControls methods to interact with the set of request controls after the request has been created.

Note that while the LDAP SDK does not provide an explicit UnboundRequest object, you can include one or more controls in an unbind request by calling the close(Control[] controls) method in the LDAPConnection class.

Accessing Response Controls

Whenever an operation completes successfully, it will return an LDAPResult object (or one of its subclasses, like BindResult). The set of response controls provided by the server can be obtained using the getResponseControls method on that LDAPResult object.

If an operation does not complete successfully, then the SDK may instead throw an LDAPException (or one of its subclasses, like LDAPSearchException). In this case, the set of response controls provided by the server can be obtained using the getResponseControls method on that LDAPException object.

Search operations may cause the server to send multiple response messages to the client (one for each entry or reference, and then a final search result done message to indicate that the search is complete). When the search completes, the SearchResult object returned (or LDAPSearchException that is thrown) will have the controls included in the search result done message, and they can be obtained using the getResponseControls method as described above. However, each search result entry or search result reference may include its own set of controls, and the getControls method may be used to obtain that set of controls from the associated SearchResultEntry or SearchResultReference object.

Note that whenever the LDAP SDK receives a response containing one or more controls, it will attempt to decode that control as the most specific type of object that it can. For example, if a response control has an OID of "2.16.840.1.113730.3.4.15", then the LDAP SDK will attempt to decode it as an AuthorizationIdentityResponseControl rather than just as a generic Control object. This will automatically happen for all types of response controls supported by the UnboundID LDAP SDK for Java, but if you define your own custom control for use with the SDK, then you may call the Control.registerDecodableControl method to allow the SDK to attempt to perform this specific decoding for that custom response control.

The Authorization Identity Controls

The authorization identity request and response controls are defined in RFC 3829 and may be used by the server to request information about the new authorization identity for a client connection after a bind operation is processed.

The AuthorizationIdentityRequestControl may be included in a bind request to indicate that the server should include a corresponding AuthorizationIdentityResponseControl in the bind response. If the bind is successful, then the bind response may include this response control and its getAuthorizationID method may be used to obtain the authorization ID returned by the server.

For example, the following code may be used to authenticate to the server and try to retrieve the authorization identity from the response:

String authzID = null;
BindRequest bindRequest =
     new SimpleBindRequest("uid=test.user,ou=People,dc=example,dc=com",
          "password", new AuthorizationIdentityRequestControl());

BindResult bindResult = connection.bind(bindRequest);
AuthorizationIdentityResponseControl authzIdentityResponse =
     AuthorizationIdentityResponseControl.get(bindResult);
if (authzIdentityResponse != null)
{
  authzID = authzIdentityResponse.getAuthorizationID();
}

The LDAP Assertion Request Control

The LDAP assertion request control is defined in RFC 4528. It provides the ability to define a search filter that must match the target entry in order for the associated operation to be processed. If the target entry does not match the assertion filter, then the server should not process that operation but instead return a response with the "Authorization Failed" result (which in most cases will cause the SDK to throw an LDAPException for the associated operation). There is no corresponding response control.

For example, the following code may be used to modify an entry to set an entry's "accountBalance" value to "543.21" only if the current value is "1234.56" (which can help prevent against race conditions that result from multiple concurrent changes to the value):

Modification mod = new Modification(ModificationType.REPLACE,
     "accountBalance", "543.21");
ModifyRequest modifyRequest =
     new ModifyRequest("uid=john.doe,ou=People,dc=example,dc=com", mod);
modifyRequest.addControl(
     new AssertionRequestControl("(accountBalance=1234.56)"));

LDAPResult modifyResult;
try
{
  modifyResult = connection.modify(modifyRequest);
  // If we've gotten here, then the modification was successful.
}
catch (LDAPException le)
{
  modifyResult = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
  if (resultCode == ResultCode.ASSERTION_FAILED)
  {
    // The modification failed because the account balance value wasn't
    // what we thought it was.
  }
  else
  {
    // The modification failed for some other reason.
  }
}

The LDAP Read Entry Controls

The LDAP read entry controls are defined in RFC 4527 and make it possible to retrieve the contents of an entry either immediately before or immediately after processing an operation. The pre-read request control may be used with delete, modify, and modify DN operations to retrieve the entry as it appeared immediately before the operation, and the post-read control may be used with the add, modify, and modify DN operations to retrieve the entry as it appeared immediately after the operation.

The PreReadRequestControl and PostReadRequestControl objects are very similar, and may specify an optional set of attributes to be included in the entry that is returned. Similarly, the corresponding PreReadResponseControl and PostReadResponseControl objects are also nearly identical, and provide access to the requested entry through the getEntry method.

For example, the following code will increment the value of the "test-counter" attribute by one and will then use the post-read controls to determine what the new value is:

// Create a modify request that we can use to increment the value of a
// custom attribute named "test-counter".
ModifyRequest modifyRequest = new ModifyRequest(
     "uid=test.user,ou=People,dc=example,dc=com",
     new Modification(ModificationType.INCREMENT,
          "test-counter", // The attribute to increment.
          "1")); // The amount by which to increment the value.

// Update the modify request to add both pre-read and post-read request
// controls to see what the entry value was before and after the change.
// We only care about getting the test-counter attribute.
modifyRequest.setControls(
     new PreReadRequestControl("test-counter"),
     new PostReadRequestControl("test-counter"));

// Process the modify operation in the server.
LDAPResult modifyResult;
try
{
  modifyResult = connection.modify(modifyRequest);
  // If we got here, then the modification should have been successful.
}
catch (LDAPException le)
{
  // This indicates that the operation did not complete successfully.
  modifyResult = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
}
LDAPTestUtils.assertResultCodeEquals(modifyResult, ResultCode.SUCCESS);

// Get the pre-read and post-read response controls from the server and
// retrieve the before and after values for the test-counter attribute.
LDAPTestUtils.assertHasControl(modifyResult,
     PreReadResponseControl.PRE_READ_RESPONSE_OID);
PreReadResponseControl preReadResponse =
     PreReadResponseControl.get(modifyResult);
Integer beforeValue =
     preReadResponse.getEntry().getAttributeValueAsInteger("test-counter");

LDAPTestUtils.assertHasControl(modifyResult,
     PostReadResponseControl.POST_READ_RESPONSE_OID);
PostReadResponseControl postReadResponse =
     PostReadResponseControl.get(modifyResult);
Integer afterValue =
     postReadResponse.getEntry().getAttributeValueAsInteger("test-counter");

The LDAP Subentries Request Control

The LDAP subentries request control is defined in draft-ietf-ldup-subentry, and may be used to indicate that a search operation should include matching entries that have the ldapSubentry object class (which are normally excluded from search results). There is no corresponding response control.

The following example illustrates the use of the LDAP subentries request control to retrieve a known subentry that will not be retrieved with a normal search.

// First, perform a search to retrieve an entry with a cn of "test subentry"
// but without including the subentries request control.  This should not
// return any matching entries.
SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
     SearchScope.SUB, Filter.createEqualityFilter("cn", "test subentry"));
SearchResult resultWithoutControl = connection.search(searchRequest);
LDAPTestUtils.assertResultCodeEquals(resultWithoutControl,
     ResultCode.SUCCESS);
LDAPTestUtils.assertEntriesReturnedEquals(resultWithoutControl, 0);

// Update the search request to add a subentries request control so that
// subentries should be included in search results.  This should cause the
// subentry to be returned.
searchRequest.addControl(new SubentriesRequestControl());
SearchResult resultWithControl = connection.search(searchRequest);
LDAPTestUtils.assertResultCodeEquals(resultWithControl, ResultCode.SUCCESS);
LDAPTestUtils.assertEntriesReturnedEquals(resultWithControl, 1);

The ManageDsaIT Request Control

The ManageDsaIT request control is defined in RFC 3296 and may be used to request that the directory server treat all entries as if they were regular entries. There is no corresponding response control.

For example, if the entry "ou=referral entry,dc=example,dc=com" is actually a smart referral that points to an entry on another server, then normal attempts to interact with that entry would cause the server to send a referral informing the client that it should send the request to the other server. If you really do want to interact with the "ou=referral entry,dc=example,dc=com" smart referral entry (e.g., to delete or update the referral), then it will be necessary to include the ManageDsaIT request control, as follows:

// Establish a connection to the directory server.  Even though it's the
// default behavior, we'll explicitly configure the connection to not follow
// referrals.
LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
connectionOptions.setFollowReferrals(false);
LDAPConnection connection = new LDAPConnection(connectionOptions,
     serverAddress, serverPort, bindDN, bindPassword);

// Try to delete an entry that will result in a referral.  Without the
// ManageDsaIT request control, we should get an exception.
DeleteRequest deleteRequest =
     new DeleteRequest("ou=referral entry,dc=example,dc=com");
LDAPResult deleteResult;
try
{
  deleteResult = connection.delete(deleteRequest);
}
catch (LDAPException le)
{
  // This exception is expected because we should get a referral, and
  // the connection is configured to not follow referrals.
  deleteResult = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
  String[] referralURLs = le.getReferralURLs();
}
LDAPTestUtils.assertResultCodeEquals(deleteResult, ResultCode.REFERRAL);
LDAPTestUtils.assertHasReferral(deleteResult);

// Update the delete request to include the ManageDsaIT request control,
// which will cause the server to try to delete the referral entry instead
// of returning a referral response.  We'll assume that the delete is
// successful.
deleteRequest.addControl(new ManageDsaITRequestControl());
try
{
  deleteResult = connection.delete(deleteRequest);
}
catch (LDAPException le)
{
  // The delete shouldn't trigger a referral, but it's possible that the
  // operation failed for some other reason (e.g., entry doesn't exist, the
  // user doesn't have permission to delete it, etc.).
  deleteResult = le.toLDAPResult();
}
LDAPTestUtils.assertResultCodeEquals(deleteResult, ResultCode.SUCCESS);
LDAPTestUtils.assertMissingReferral(deleteResult);

connection.close();

The Matched Values Control

The matched values request control is defined in RFC 3876 and may be used in a search operation to request that only attribute values matching one or more filters should be included in matching entries. This can be useful, for example, if an attribute has a large number of values and you are only interested in values matching a specified set of criteria. There is no corresponding response control.

The following code demonstrates the use of the matched values control to retrieve only a portion of the values for an attribute:

// Ensure that a test user has multiple description values.
LDAPResult modifyResult = connection.modify(
     "uid=test.user,ou=People,dc=example,dc=com",
     new Modification(ModificationType.REPLACE,
          "description", // Attribute name
          "first", "second", "third", "fourth")); // Attribute values.
assertResultCodeEquals(modifyResult, ResultCode.SUCCESS);

// Perform a search to retrieve the test user entry without using the
// matched values request control.  This should return all four description
// values.
SearchRequest searchRequest = new SearchRequest(
     "uid=test.user,ou=People,dc=example,dc=com", // Base DN
     SearchScope.BASE, // Scope
     Filter.createPresenceFilter("objectClass"), // Filter
     "description"); // Attributes to return.
SearchResultEntry entryRetrievedWithoutControl =
     connection.searchForEntry(searchRequest);
Attribute fullDescriptionAttribute =
     entryRetrievedWithoutControl.getAttribute("description");
int numFullDescriptionValues = fullDescriptionAttribute.size();

// Update the search request to include a matched values control that will
// only return values that start with the letter "f".  In our test entry,
// this should just match two values ("first" and "fourth").
searchRequest.addControl(new MatchedValuesRequestControl(
     MatchedValuesFilter.createSubstringFilter("description", // Attribute
          "f", // subInitial component
          null, // subAny components
          null))); // subFinal component
SearchResultEntry entryRetrievedWithControl =
     connection.searchForEntry(searchRequest);
Attribute partialDescriptionAttribute =
     entryRetrievedWithControl.getAttribute("description");
int numPartialDescriptionValues = partialDescriptionAttribute.size();

The Password Expired and Password Expiring Response Controls

The password expired and password expiring controls are defined in draft-vchu-ldap-pwd-policy and may be returned by the server with a bind response to indicate that the user's password either is expired or is about to expire. If the password is about to expire, then the password expiring response control will include the length of time until the password actually expires.

For example:

// Send a simple bind request to the directory server.
BindRequest bindRequest =
     new SimpleBindRequest("uid=test.user,ou=People,dc=example,dc=com",
          "password");
BindResult bindResult;
boolean bindSuccessful;
boolean passwordExpired;
boolean passwordAboutToExpire;
try
{
  bindResult = connection.bind(bindRequest);

  // If we got here, the bind was successful and we know the password was
  // not expired.  However, we shouldn't ignore the result because the
  // password might be about to expire.  To determine whether that is the
  // case, we should see if the bind result included a password expiring
  // control.
  bindSuccessful = true;
  passwordExpired = false;

  PasswordExpiringControl expiringControl =
       PasswordExpiringControl.get(bindResult);
  if (expiringControl != null)
  {
    passwordAboutToExpire = true;
    int secondsToExpiration = expiringControl.getSecondsUntilExpiration();
  }
  else
  {
    passwordAboutToExpire = false;
  }
}
catch (LDAPException le)
{
  // If we got here, then the bind failed.  The failure may or may not have
  // been due to an expired password.  To determine that, we should see if
  // the bind result included a password expired control.
  bindSuccessful = false;
  passwordAboutToExpire = false;
  bindResult = new BindResult(le.toLDAPResult());
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();

  PasswordExpiredControl expiredControl =
       PasswordExpiredControl.get(bindResult);
  if (expiredControl != null)
  {
    passwordExpired = true;
  }
  else
  {
    passwordExpired = false;
  }
}

The Persistent Search and Entry Change Notification Controls

The persistent search request control is defined in draft-ietf-ldapext-psearch and may be included in a search request to indicate that the server should send a search result entry message each time an entry matching the associated search criteria is updated in the server. This type of search is somewhat unique in that it generally doesn't end on its own, and it can also return the same entry multiple times (or at least different versions of that entry) if the same entry is updated multiple times.

Note that because persistent searches don't necessarily have a defined end and you will want to be able to do something with entries as soon as they are updated, then you should only use the persistent search request control in a search that uses a SearchResultListener to handle the entries that get returned. Those entries may optionally contain the entry change notification control, which can include additional information about the update, including the type of operation (add, delete, modify, or modify DN), and potentially the change number and/or previous DN (if it was a modify DN operation).

For example, the following code will begin an asynchronous search including the persistent search control that will notify the client of all changes to entries at or below "dc=example,dc=com".

SearchRequest persistentSearchRequest = new SearchRequest(
     asyncSearchListener, "dc=example,dc=com", SearchScope.SUB,
     Filter.createPresenceFilter("objectClass"));
persistentSearchRequest.addControl(new PersistentSearchRequestControl(
     PersistentSearchChangeType.allChangeTypes(), // Notify change types.
     true, // Only return new changes, don't match existing entries.
     true)); // Include change notification controls in search entries.

// Launch the persistent search as an asynchronous operation.
AsyncRequestID persistentSearchRequestID =
     connection.asyncSearch(persistentSearchRequest);

// Modify an entry that matches the persistent search criteria.  This
// should cause the persistent search listener to be notified.
LDAPResult modifyResult = connection.modify(
     "uid=test.user,ou=People,dc=example,dc=com",
     new Modification(ModificationType.REPLACE, "description", "test"));

// Verify that the persistent search listener was notified....

// Since persistent search operations don't end on their own, we need to
// abandon the search when we don't need it anymore.
connection.abandon(persistentSearchRequestID);

The Proxied Authorization Request Controls

The proxied authorization request controls may be used to request that an operation be processed under the authority of another user. Proxied authorization is frequently used in conjunction with connection pools because it allows the client to maintain a set of connections authenticated as a given user (which has permission to use the proxied authorization control) and use those connections to perform operations in the server under the authority of the actual end user that triggered the request. There is no corresponding response control.

The UnboundID LDAP SDK for Java supports two different versions of the proxied authorization control. A number of directory servers implement support for the proxied authorization V1 control, which is defined in early versions of the draft-weltman-ldapv3-proxy Internet Draft. More recently, however, this specification has been updated and released as RFC 4370 to define the proxied authorization V2 control. The two controls have different OIDs, and also take different arguments. The target user for the proxied authorization V1 control must be a distinguished name, whereas the target user for the proxied authorization V2 control must be an authorization ID (as defined in section 9 of RFC 2829, e.g., "dn:uid=john.doe,ou=People,dc=example,dc=com" or "u:john.doe"). In general, it is recommended to use the proxied authorization V2 control in preference to V1 when possible, since it has a more well-defined specification.

For example, the following code demonstrates the use of the proxied authorization V2 request control to delete an entry under the authority of the user with DN "uid=alternate.user,ou=People,dc=example,dc=com":

// Create a delete request to delete an entry.  Include the proxied
// authorization v2 request control in the delete request so that the
// delete will be processed as the user with username "alternate.user"
// instead of the user that's actually authenticated on the connection.
DeleteRequest deleteRequest =
     new DeleteRequest("uid=test.user,ou=People,dc=example,dc=com");
deleteRequest.addControl(new ProxiedAuthorizationV2RequestControl(
     "u:alternate.user"));

LDAPResult deleteResult;
try
{
  deleteResult = connection.delete(deleteRequest);
  // If we got here, then the delete was successful.
}
catch (LDAPException le)
{
  // The delete failed for some reason.  In addition to all of the normal
  // reasons a delete could fail (e.g., the entry doesn't exist, or has one
  // or more subordinates), proxied-authorization specific failures may
  // include that the authenticated user doesn't have permission to use the
  // proxied authorization control to impersonate the alternate user, that
  // the alternate user doesn't exist, or that the alternate user doesn't
  // have permission to perform the requested operation.
  deleteResult = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
}

The Server-Side Sort Controls

The server-side sort request control may be used to request that the server sort the set of matching entries before returning them to the client. A corresponding response control can provide information about the result of the sort processing.

Note that this can be an expensive operation in some cases, so LDAP client developers that may wish to use this feature should first discuss it with an administrator of the target directory to determine whether that is appropriate and if the server is appropriately configured to process such requests efficiently. As an alternative, it may be desirable to perform client-side sorting, which will significantly reduce the load on the server.

The following example demonstrates the use of the server-side sort controls to request entries matching a search request in varying orders:

// Perform a search to get all user entries sorted by last name, then by
// first name, both in ascending order.
SearchRequest searchRequest = new SearchRequest(
     "ou=People,dc=example,dc=com", SearchScope.SUB,
     Filter.createEqualityFilter("objectClass", "person"));
searchRequest.addControl(new ServerSideSortRequestControl(
     new SortKey("sn"), new SortKey("givenName")));
SearchResult lastNameAscendingResult;
try
{
  lastNameAscendingResult = connection.search(searchRequest);
  // If we got here, then the search was successful.
}
catch (LDAPSearchException lse)
{
  // The search failed for some reason.
  lastNameAscendingResult = lse.getSearchResult();
  ResultCode resultCode = lse.getResultCode();
  String errorMessageFromServer = lse.getDiagnosticMessage();
}

// Get the response control and retrieve the result code for the sort
// processing.
LDAPTestUtils.assertHasControl(lastNameAscendingResult,
     ServerSideSortResponseControl.SERVER_SIDE_SORT_RESPONSE_OID);
ServerSideSortResponseControl lastNameAscendingResponseControl =
     ServerSideSortResponseControl.get(lastNameAscendingResult);
ResultCode lastNameSortResult =
     lastNameAscendingResponseControl.getResultCode();


// Perform the same search, but this time request the results to be sorted
// in descending order by first name, then last name.
searchRequest.setControls(new ServerSideSortRequestControl(
     new SortKey("givenName", true), new SortKey("sn", true)));
SearchResult firstNameDescendingResult;
try
{
  firstNameDescendingResult = connection.search(searchRequest);
  // If we got here, then the search was successful.
}
catch (LDAPSearchException lse)
{
  // The search failed for some reason.
  firstNameDescendingResult = lse.getSearchResult();
  ResultCode resultCode = lse.getResultCode();
  String errorMessageFromServer = lse.getDiagnosticMessage();
}

// Get the response control and retrieve the result code for the sort
// processing.
LDAPTestUtils.assertHasControl(firstNameDescendingResult,
     ServerSideSortResponseControl.SERVER_SIDE_SORT_RESPONSE_OID);
ServerSideSortResponseControl firstNameDescendingResponseControl =
     ServerSideSortResponseControl.get(firstNameDescendingResult);
ResultCode firstNameSortResult =
     firstNameDescendingResponseControl.getResultCode();

The Simple Paged Results Control

The simple paged results control is defined in RFC 2696 and allows the client to read "pages" of results (where each "page" contains a subset of the overall set of matching entries). It is similar to the virtual list view control discussed below, although it does not provide as many options, but also does not include the constraint that the results be sorted. Some servers support the simple paged results control but not the virtual list view control, or vice versa, so it is recommended to contact your directory administrator to determine what options are available.

The simple paged results control is both a request and response control. On the first request to the server, it should contain a page size but no "cookie" value. When the server has finished sending the specified number of entries, it will include the simple paged results control in the search result done message, and if there are more results to return then that response will include a cookie value that can be used to help the server figure out where to resume processing for the next page of results.

The following code demonstrates the use of the simple paged results control to return all users in the server, retrieving up to 10 entries at a time:

// Perform a search to retrieve all users in the server, but only retrieving
// ten at a time.
int numSearches = 0;
int totalEntriesReturned = 0;
SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
     SearchScope.SUB, Filter.createEqualityFilter("objectClass", "person"));
ASN1OctetString resumeCookie = null;
while (true)
{
  searchRequest.setControls(
       new SimplePagedResultsControl(10, resumeCookie));
  SearchResult searchResult = connection.search(searchRequest);
  numSearches++;
  totalEntriesReturned += searchResult.getEntryCount();
  for (SearchResultEntry e : searchResult.getSearchEntries())
  {
    // Do something with each entry...
  }

  LDAPTestUtils.assertHasControl(searchResult,
       SimplePagedResultsControl.PAGED_RESULTS_OID);
  SimplePagedResultsControl responseControl =
       SimplePagedResultsControl.get(searchResult);
  if (responseControl.moreResultsToReturn())
  {
    // The resume cookie can be included in the simple paged results
    // control included in the next search to get the next page of results.
    resumeCookie = responseControl.getCookie();
  }
  else
  {
    break;
  }
}

The Subtree Delete Request Control

The subtree delete request control is defined in draft-armijo-ldap-treedelete and may be used to request that the server should delete the specified entry and all of its subordinates. Without this control, if an entry has one or more subordinates, then the server will return a "not allowed on non-leaf" result and will refuse to process the delete. There is no corresponding response control.

Note that performing a subtree delete on an entry with a large number of subordinates can be a very expensive operation. If you wish to use this control on a large subtree, then it is recommended that you first discuss it with the directory administrator.

The following example demonstrates the use of the subtree delete control:

// First, try to delete an entry that has children, but don't include the
// subtree delete control.  This delete attempt should fail, and the
// "NOT_ALLOWED_ON_NONLEAF" result is most appropriate if the failure reason
// is that the entry has subordinates.
DeleteRequest deleteRequest =
     new DeleteRequest("ou=entry with children,dc=example,dc=com");
LDAPResult resultWithoutControl;
try
{
  resultWithoutControl = connection.delete(deleteRequest);
  // We shouldn't get here because the delete should fail.
}
catch (LDAPException le)
{
  // This is expected because the entry has children.
  resultWithoutControl = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
}
LDAPTestUtils.assertResultCodeEquals(resultWithoutControl,
     ResultCode.NOT_ALLOWED_ON_NONLEAF);

// Update the delete request to include the subtree delete request control
// and try again.
deleteRequest.addControl(new SubtreeDeleteRequestControl());
LDAPResult resultWithControl;
try
{
  resultWithControl = connection.delete(deleteRequest);
  // The delete should no longer be rejected just because the target entry
  // has children.
}
catch (LDAPException le)
{
  // The delete still failed for some other reason.
  resultWithControl = le.toLDAPResult();
  ResultCode resultCode = le.getResultCode();
  String errorMessageFromServer = le.getDiagnosticMessage();
}
LDAPTestUtils.assertResultCodeEquals(resultWithControl, ResultCode.SUCCESS);

The Virtual List View Controls

The virtual list view controls are defined in draft-ietf-ldapext-ldapv3-vlv, and may be used to retrieve arbitrary pages of results matching the given search criteria. It is similar to the simple paged results control, but requires that the search request also include the server-side sort request (which is not a requirement with the simple paged results control) and also allows the client to request arbitrary pages of the results (whereas the simple paged results control only allows sequential iteration through the result pages). The page of results to be returned can be specified either by offset (the position of the target entry in the sorted result set) or based on the value of the primary sort key (e.g., start at the entry with a primary sort key value greater than or equal to "k").

Another benefit of the virtual list view control over the simple paged results control is that it can provide the client with an estimate of the total number of entries that match the search. This is important to help the client know when it has reached the end of the results.

The following example illustrates the use of the virtual list view request control to return all users in server, retrieving up to 10 entries at a time:

// Perform a search to retrieve all users in the server, but only retrieving
// ten at a time.  Ensure that the users are sorted in ascending order by
// last name, then first name.
int numSearches = 0;
int totalEntriesReturned = 0;
SearchRequest searchRequest = new SearchRequest("dc=example,dc=com",
     SearchScope.SUB, Filter.createEqualityFilter("objectClass", "person"));
int vlvOffset = 1;
int vlvContentCount = 0;
ASN1OctetString vlvContextID = null;
while (true)
{
  // Note that the VLV control always requires the server-side sort
  // control.
  searchRequest.setControls(
       new ServerSideSortRequestControl(new SortKey("sn"),
            new SortKey("givenName")),
       new VirtualListViewRequestControl(vlvOffset, 0, 9, vlvContentCount,
            vlvContextID));
  SearchResult searchResult = connection.search(searchRequest);
  numSearches++;
  totalEntriesReturned += searchResult.getEntryCount();
  for (SearchResultEntry e : searchResult.getSearchEntries())
  {
    // Do something with each entry...
  }

  LDAPTestUtils.assertHasControl(searchResult,
       VirtualListViewResponseControl.VIRTUAL_LIST_VIEW_RESPONSE_OID);
  VirtualListViewResponseControl vlvResponseControl =
       VirtualListViewResponseControl.get(searchResult);
  vlvContentCount = vlvResponseControl.getContentCount();
  vlvOffset += 10;
  vlvContextID = vlvResponseControl.getContextID();
  if (vlvOffset > vlvContentCount)
  {
    break;
  }
}