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