From 060013ad7b5e6d9f4ca1e2db098ad6d762109588 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 17 Oct 2013 12:50:31 +0000
Subject: [PATCH] Fix OPENDJ-701: Implement paged results support
---
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java | 112 +++++++++++++++++++++++++++++++++++++
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 58 ++++++++++++++++++-
2 files changed, 166 insertions(+), 4 deletions(-)
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index 80f0ef2..69742ff 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -60,8 +60,10 @@
import org.forgerock.json.resource.UpdateRequest;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
+import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
@@ -75,6 +77,7 @@
import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
@@ -92,6 +95,9 @@
// Dummy exception used for signalling search success.
private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
+ // Empty decode options required for decoding response controls.
+ private static final DecodeOptions DECODE_OPTIONS = new DecodeOptions();
+
private final List<Attribute> additionalLDAPAttributes;
private final AttributeMapper attributeMapper;
private final DN baseDN; // TODO: support template variables.
@@ -342,11 +348,40 @@
} else {
// Perform the search.
final String[] attributes = getLDAPAttributes(c, request.getFields());
- final SearchRequest request =
+ final SearchRequest searchRequest =
newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL,
ldapFilter == Filter.alwaysTrue() ? Filter
.objectClassPresent() : ldapFilter, attributes);
- c.getConnection().searchAsync(request, null, new SearchResultHandler() {
+
+ /*
+ * Add the page results control. We can support the
+ * page offset by reading the next offset pages, or
+ * offset x page size resources.
+ */
+ final int pageResultStartIndex;
+ final int pageSize = request.getPageSize();
+ if (request.getPageSize() > 0) {
+ final int pageResultEndIndex;
+ if (request.getPagedResultsOffset() > 0) {
+ pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
+ pageResultEndIndex = pageResultStartIndex + pageSize;
+ } else {
+ pageResultStartIndex = 0;
+ pageResultEndIndex = pageSize;
+ }
+ final ByteString cookie =
+ request.getPagedResultsCookie() != null ? ByteString
+ .valueOfBase64(request.getPagedResultsCookie())
+ : ByteString.empty();
+ final SimplePagedResultsControl control =
+ SimplePagedResultsControl.newControl(true,
+ pageResultEndIndex, cookie);
+ searchRequest.addControl(control);
+ } else {
+ pageResultStartIndex = 0;
+ }
+
+ c.getConnection().searchAsync(searchRequest, null, new SearchResultHandler() {
/*
* The following fields are guarded by
* sequenceLock. In addition, the sequenceLock
@@ -357,6 +392,8 @@
private int pendingResourceCount = 0;
private ResourceException pendingResult = null;
private boolean resultSent = false;
+ private int totalResourceCount = 0;
+ private String cookie = null;
@Override
public boolean handleEntry(final SearchResultEntry entry) {
@@ -371,6 +408,10 @@
if (pendingResult != null) {
return false;
}
+ if (totalResourceCount++ < pageResultStartIndex) {
+ // Haven't reached paged results threshold yet.
+ return true;
+ }
pendingResourceCount++;
}
@@ -440,6 +481,17 @@
@Override
public void handleResult(final Result result) {
synchronized (sequenceLock) {
+ if (request.getPageSize() > 0) {
+ try {
+ final SimplePagedResultsControl control = result.getControl(
+ SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
+ if (control != null && !control.getCookie().isEmpty()) {
+ cookie = control.getCookie().toBase64String();
+ }
+ } catch (final DecodeException e) {
+ // FIXME: need some logging.
+ }
+ }
completeIfNecessary(SUCCESS);
}
}
@@ -465,7 +517,7 @@
if (pendingResourceCount == 0 && pendingResult != null
&& !resultSent) {
if (pendingResult == SUCCESS) {
- h.handleResult(new QueryResult());
+ h.handleResult(new QueryResult(cookie, -1));
} else {
h.handleError(pendingResult);
}
diff --git a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index c195206..d459ffe 100644
--- a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -28,6 +28,7 @@
import static org.forgerock.json.resource.PatchOperation.replace;
import static org.forgerock.json.resource.Requests.newDeleteRequest;
import static org.forgerock.json.resource.Requests.newPatchRequest;
+import static org.forgerock.json.resource.Requests.newQueryRequest;
import static org.forgerock.json.resource.Requests.newReadRequest;
import static org.forgerock.json.resource.Requests.newUpdateRequest;
import static org.forgerock.json.resource.Resources.newCollection;
@@ -42,6 +43,7 @@
import static org.forgerock.opendj.rest2ldap.TestUtils.ctx;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -51,6 +53,9 @@
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.NotSupportedException;
import org.forgerock.json.resource.PreconditionFailedException;
+import org.forgerock.json.resource.QueryFilter;
+import org.forgerock.json.resource.QueryResult;
+import org.forgerock.json.resource.Requests;
import org.forgerock.json.resource.Resource;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.IntermediateResponseHandler;
@@ -88,6 +93,84 @@
// FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend).
@Test
+ public void testQueryAll() throws Exception {
+ final Connection connection = newConnection();
+ final List<Resource> resources = new LinkedList<Resource>();
+ final QueryResult result =
+ connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
+ QueryFilter.alwaysTrue()), resources);
+ assertThat(resources).hasSize(5);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testQueryNone() throws Exception {
+ final Connection connection = newConnection();
+ final List<Resource> resources = new LinkedList<Resource>();
+ final QueryResult result =
+ connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
+ QueryFilter.alwaysFalse()), resources);
+ assertThat(resources).hasSize(0);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testQueryPageResultsCookie() throws Exception {
+ final Connection connection = newConnection();
+ final List<Resource> resources = new ArrayList<Resource>();
+
+ // Read first page.
+ QueryResult result =
+ connection.query(ctx(), newQueryRequest("")
+ .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2), resources);
+ assertThat(result.getPagedResultsCookie()).isNotNull();
+ assertThat(resources).hasSize(2);
+ assertThat(resources.get(0).getId()).isEqualTo("test1");
+ assertThat(resources.get(1).getId()).isEqualTo("test2");
+
+ String cookie = result.getPagedResultsCookie();
+ resources.clear();
+
+ // Read second page.
+ result =
+ connection.query(ctx(), newQueryRequest("")
+ .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
+ .setPagedResultsCookie(cookie), resources);
+ assertThat(result.getPagedResultsCookie()).isNotNull();
+ assertThat(resources).hasSize(2);
+ assertThat(resources.get(0).getId()).isEqualTo("test3");
+ assertThat(resources.get(1).getId()).isEqualTo("test4");
+
+ cookie = result.getPagedResultsCookie();
+ resources.clear();
+
+ // Read third page.
+ result =
+ connection.query(ctx(), newQueryRequest("")
+ .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
+ .setPagedResultsCookie(cookie), resources);
+ assertThat(result.getPagedResultsCookie()).isNull();
+ assertThat(resources).hasSize(1);
+ assertThat(resources.get(0).getId()).isEqualTo("test5");
+ }
+
+ @Test
+ public void testQueryPageResultsIndexed() throws Exception {
+ final Connection connection = newConnection();
+ final List<Resource> resources = new ArrayList<Resource>();
+ QueryResult result =
+ connection.query(ctx(), newQueryRequest("")
+ .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
+ .setPagedResultsOffset(1), resources);
+ assertThat(result.getPagedResultsCookie()).isNotNull();
+ assertThat(resources).hasSize(2);
+ assertThat(resources.get(0).getId()).isEqualTo("test3");
+ assertThat(resources.get(1).getId()).isEqualTo("test4");
+ }
+
+ @Test
public void testDelete() throws Exception {
final Connection connection = newConnection();
final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
@@ -601,7 +684,34 @@
"userpassword: password",
"cn: test user 2",
"sn: user 2",
- "etag: 67890"
+ "etag: 67890",
+ "",
+ "dn: uid=test3,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: test3",
+ "userpassword: password",
+ "cn: test user 3",
+ "sn: user 3",
+ "etag: 33333",
+ "",
+ "dn: uid=test4,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: test4",
+ "userpassword: password",
+ "cn: test user 4",
+ "sn: user 4",
+ "etag: 44444",
+ "",
+ "dn: uid=test5,dc=test",
+ "objectClass: top",
+ "objectClass: person",
+ "uid: test5",
+ "userpassword: password",
+ "cn: test user 5",
+ "sn: user 5",
+ "etag: 55555"
));
// @formatter:on
--
Gitblit v1.10.0