From 6f092b277fa084796db106dfe497040c38db23a7 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 02 Apr 2015 00:16:00 +0000
Subject: [PATCH] OPENDJ-1711 - re-implement VLV support for pluggable backends

---
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java          |  201 ++++++++----
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/CursorTransformer.java |    7 
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java    |    3 
 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Cursor.java        |    8 
 opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ControlsTestCase.java  |  695 +++++++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java  |   22 -
 6 files changed, 830 insertions(+), 106 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
index 06d8913..0d8543d 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/persistit/PersistItStorage.java
@@ -180,11 +180,11 @@
     public boolean positionToIndex(int index)
     {
       // There doesn't seem to be a way to optimize this using Persistit.
+      clearCurrentKeyAndValue();
+      ex.getKey().to(Key.BEFORE);
       try
       {
-        clearCurrentKeyAndValue();
-        ex.getKey().to(Key.BEFORE);
-        for (int i = 0; i < index; i++)
+        for (int i = 0; i <= index; i++)
         {
           if (!ex.next())
           {
@@ -202,22 +202,8 @@
     @Override
     public boolean positionToLastKey()
     {
-      try
-      {
-        clearCurrentKeyAndValue();
-        ex.getKey().to(Key.AFTER);
-        return ex.previous();
-      }
-      catch (final PersistitException e)
-      {
-        throw new StorageRuntimeException(e);
-      }
-    }
-
-    @Override
-    public boolean previous()
-    {
       clearCurrentKeyAndValue();
+      ex.getKey().to(Key.AFTER);
       try
       {
         return ex.previous();
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/CursorTransformer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/CursorTransformer.java
index 5e37f9c..aa38f18 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/CursorTransformer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/CursorTransformer.java
@@ -172,13 +172,6 @@
     return input.positionToIndex(index);
   }
 
-  @Override
-  public boolean previous()
-  {
-    clearCache();
-    return input.previous();
-  }
-
   private void clearCache()
   {
     cachedTransformedKey = null;
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
index 15db2a5..7c2743c 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -3237,7 +3237,6 @@
     {
       return sortByOffset(searchOperation, vlvRequest, sortMap);
     }
-
     return sortByGreaterThanOrEqualAssertion(searchOperation, vlvRequest, sortOrder, sortMap);
   }
 
@@ -3295,7 +3294,7 @@
     if (targetFound)
     {
       final long[] array = new long[index - startIndex];
-      System.arraycopy(idSet, startIndex, array, 0, index);
+      System.arraycopy(idSet, startIndex, array, 0, array.length);
       result = newDefinedSet(array);
     }
     else
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
index 62426db..0c865cc 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/VLVIndex.java
@@ -35,6 +35,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.TreeSet;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -515,26 +516,73 @@
     final int currentCount = count.get();
     final int beforeCount = vlvRequest.getBeforeCount();
     final int afterCount = vlvRequest.getAfterCount();
-
     final ByteString assertion = vlvRequest.getGreaterThanOrEqualAssertion();
     final ByteSequence encodedTargetAssertion =
         encodeTargetAssertion(sortOrder, assertion, searchOperation, currentCount);
-    final Cursor cursor = txn.openCursor(getName());
+    @SuppressWarnings("unchecked")
+    final Cursor<ByteString, ByteString> cursor = txn.openCursor(getName());
     try
     {
-      if (!cursor.positionToKeyOrNext(encodedTargetAssertion))
+      final LinkedList<Long> selectedIDs = new LinkedList<Long>();
+      int targetPosition = 0;
+
+      // Don't waste cycles looking for an assertion that does not match anything.
+      if (cursor.positionToKeyOrNext(encodedTargetAssertion) && cursor.positionToIndex(0))
       {
-        return newDefinedSet();
+        /*
+         * Unfortunately we need to iterate from the start of the index in order to correctly
+         * calculate the target position.
+         */
+        boolean targetFound = false;
+        int includedAfter = 0;
+        do
+        {
+          final ByteString key = cursor.getKey();
+          if (!targetFound)
+          {
+            selectedIDs.add(decodeEntryIDFromVLVKey(key));
+            if (encodedTargetAssertion.compareTo(key) > 0)
+            {
+              if (targetPosition >= beforeCount)
+              {
+                // Strip out unwanted results.
+                selectedIDs.removeFirst();
+              }
+              targetPosition++;
+            }
+            else
+            {
+              targetFound = true;
+            }
+          }
+          else if (includedAfter < afterCount)
+          {
+            selectedIDs.add(decodeEntryIDFromVLVKey(key));
+            includedAfter++;
+          }
+          else
+          {
+            break;
+          }
+        }
+        while (cursor.next());
       }
-      int count = afterCount;
-      for (int i = 0; cursor.previous() && i < beforeCount; i++, count++)
+      else
       {
-        // Empty block.
+        /*
+         * Treat an non-matching assertion as matching beyond the end of the index.
+         */
+        targetPosition = currentCount;
       }
-      final long[] selectedIDs = readRange(cursor, count, debugBuilder);
-      searchOperation.addResponseControl(new VLVResponseControl(count - afterCount, currentCount,
+      searchOperation.addResponseControl(new VLVResponseControl(targetPosition + 1, currentCount,
           LDAPResultCode.SUCCESS));
-      return newDefinedSet(selectedIDs);
+      final long[] result = new long[selectedIDs.size()];
+      int i = 0;
+      for (Long entryID : selectedIDs)
+      {
+        result[i++] = entryID;
+      }
+      return newDefinedSet(result);
     }
     finally
     {
@@ -625,11 +673,15 @@
     final Cursor cursor = txn.openCursor(getName());
     try
     {
-      if (!cursor.positionToIndex(startPos))
+      final long[] selectedIDs;
+      if (cursor.positionToIndex(startPos))
       {
-        return newDefinedSet();
+        selectedIDs = readRange(cursor, count, debugBuilder);
       }
-      final long[] selectedIDs = readRange(cursor, count, debugBuilder);
+      else
+      {
+        selectedIDs = new long[0];
+      }
       searchOperation.addResponseControl(new VLVResponseControl(targetOffset, currentCount, LDAPResultCode.SUCCESS));
       return newDefinedSet(selectedIDs);
     }
@@ -644,22 +696,26 @@
   {
     long[] selectedIDs = new long[count];
     int selectedPos = 0;
-    while (cursor.next() && selectedPos < count)
+    if (count > 0)
     {
-      final ByteString key = cursor.getKey();
-      if (logger.isTraceEnabled())
+      do
       {
-        logSearchKeyResult(key);
+        final ByteString key = cursor.getKey();
+        if (logger.isTraceEnabled())
+        {
+          logSearchKeyResult(key);
+        }
+        selectedIDs[selectedPos++] = decodeEntryIDFromVLVKey(key);
       }
-      selectedIDs[selectedPos++] = decodeEntryIDFromVLVKey(key);
-    }
-    if (selectedPos < count)
-    {
-      /*
-       * We don't have enough entries in the set to meet the requested page size, so we'll need to
-       * shorten the array.
-       */
-      selectedIDs = Arrays.copyOf(selectedIDs, selectedPos);
+      while (selectedPos < count && cursor.next());
+      if (selectedPos < count)
+      {
+        /*
+         * We don't have enough entries in the set to meet the requested page size, so we'll need to
+         * shorten the array.
+         */
+        selectedIDs = Arrays.copyOf(selectedIDs, selectedPos);
+      }
     }
     if (debugBuilder != null)
     {
@@ -714,48 +770,41 @@
   {
     for (final SortKey sortKey : sortOrder.getSortKeys())
     {
-      final ByteString value = findBestMatchingValue(sortKey, entry);
-      final MatchingRule matchingRule = sortKey.getAttributeType().getOrderingMatchingRule();
-      ByteString normalizedAttributeValue;
-      try
+      final AttributeType attributeType = sortKey.getAttributeType();
+      final MatchingRule matchingRule = attributeType.getOrderingMatchingRule();
+      final List<Attribute> attrList = entry.getAttribute(attributeType);
+      ByteString sortValue = null;
+      if (attrList != null)
       {
-        normalizedAttributeValue = matchingRule.normalizeAttributeValue(value);
-      }
-      catch (final DecodeException e)
-      {
-        /*
-         * This shouldn't happen because the attribute should have already been validated. If it
-         * does then treat the value as missing.
-         */
-        normalizedAttributeValue = ByteString.empty();
-      }
-      encodeVLVKeyValue(sortKey, normalizedAttributeValue, builder);
-    }
-  }
-
-  /**
-   * Returns the value contained in the entry which should be used for the provided sort key. For
-   * ascending order we select the highest value from the entry, and for descending order we select
-   * the lowest.
-   */
-  private static ByteString findBestMatchingValue(final SortKey sortKey, final Entry entry)
-  {
-    final List<Attribute> attrList = entry.getAttribute(sortKey.getAttributeType());
-    ByteString sortValue = null;
-    if (attrList != null)
-    {
-      for (Attribute a : attrList)
-      {
-        for (ByteString v : a)
+        for (Attribute a : attrList)
         {
-          if (sortValue == null || sortKey.compareValues(v, sortValue) < 0)
+          for (ByteString v : a)
           {
-            sortValue = v;
+            try
+            {
+              /*
+               * The RFC states that the lowest value of a multi-valued attribute should be used,
+               * regardless of the sort order.
+               */
+              final ByteString nv = matchingRule.normalizeAttributeValue(v);
+              if (sortValue == null || nv.compareTo(sortValue) < 0)
+              {
+                sortValue = nv;
+              }
+            }
+            catch (final DecodeException e)
+            {
+              /*
+               * This shouldn't happen because the attribute should have already been validated. If
+               * it does then treat the value as missing.
+               */
+              continue;
+            }
           }
         }
       }
+      encodeVLVKeyValue(sortKey, sortValue, builder);
     }
-    return sortValue != null ? sortValue : ByteString.empty();
   }
 
   private static void encodeVLVKeyValue(final SortKey sortKey, final ByteString normalizedAttributeValue,
@@ -763,19 +812,29 @@
   {
     final boolean ascending = sortKey.ascending();
     final byte separator = ascending ? (byte) 0x00 : (byte) 0xff;
-    final byte escape = ascending ? (byte) 0x01 : (byte) 0xfe;
-    final byte sortOrderMask = separator;
-
-    final int length = normalizedAttributeValue.length();
-    for (int i = 0; i < length; i++)
+    if (normalizedAttributeValue != null)
     {
-      final byte b = normalizedAttributeValue.byteAt(i);
-      if (b == separator || b == escape)
+      // Ensure that all keys sort before (ascending) or after (descending) missing keys.
+      builder.append(separator);
+
+      final byte escape = ascending ? (byte) 0x01 : (byte) 0xfe;
+      final byte sortOrderMask = separator;
+      final int length = normalizedAttributeValue.length();
+      for (int i = 0; i < length; i++)
       {
-        builder.append(escape);
+        final byte b = normalizedAttributeValue.byteAt(i);
+        if (b == separator || b == escape)
+        {
+          builder.append(escape);
+        }
+        // Invert the bits if this key is in descending order.
+        builder.append((byte) (b ^ sortOrderMask));
       }
-      // Invert the bits if this key is in descending order.
-      builder.append((byte) (b ^ sortOrderMask));
+    }
+    else
+    {
+      // Ensure that missing keys sort after (ascending) or before (descending) all other keys.
+      builder.append(ascending ? (byte) 0xff : (byte) 0x00);
     }
     builder.append(separator);
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Cursor.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Cursor.java
index a5f84b3..48e9444 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Cursor.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/spi/Cursor.java
@@ -84,14 +84,6 @@
   boolean next();
 
   /**
-   * Moves this cursor to the previous record in the tree.
-   *
-   * @return {@code true} if the cursor could move to the previous record,
-   *         {@code false} if no previous record exists
-   */
-  boolean previous();
-
-  /**
    * Returns the key of the record on which this cursor is currently positioned.
    *
    * @return the current record's key,
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ControlsTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ControlsTestCase.java
new file mode 100644
index 0000000..85060d2
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ControlsTestCase.java
@@ -0,0 +1,695 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2015 ForgeRock AS
+ */
+package org.opends.server.backends.pluggable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.opends.server.TestCaseUtils.makeEntry;
+import static org.opends.server.TestCaseUtils.newSortedSet;
+import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
+import static org.opends.server.protocols.internal.Requests.newSearchRequest;
+import static org.opends.server.util.ServerConstants.OID_SERVER_SIDE_SORT_RESPONSE_CONTROL;
+import static org.opends.server.util.ServerConstants.OID_VLV_RESPONSE_CONTROL;
+import static org.testng.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.admin.std.meta.BackendVLVIndexCfgDefn.Scope;
+import org.opends.server.admin.std.server.BackendVLVIndexCfg;
+import org.opends.server.admin.std.server.PersistitBackendCfg;
+import org.opends.server.backends.persistit.PitBackend;
+import org.opends.server.controls.ServerSideSortRequestControl;
+import org.opends.server.controls.ServerSideSortResponseControl;
+import org.opends.server.controls.VLVRequestControl;
+import org.opends.server.controls.VLVResponseControl;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.protocols.internal.SearchRequest;
+import org.opends.server.protocols.ldap.LDAPControl;
+import org.opends.server.protocols.ldap.LDAPResultCode;
+import org.opends.server.types.Control;
+import org.opends.server.types.DN;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.SearchResultEntry;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * This class contains a number of test cases for the controls which are supported within the
+ * backend. In particular, VLV, paged results, and server side sorting controls.
+ */
+@SuppressWarnings("javadoc")
+public class ControlsTestCase extends DirectoryServerTestCase
+{
+
+  private static final String BACKEND_BASE_DN = "dc=pluggable-vlv,dc=com";
+  private static final String BACKEND_NAME = "pluggable-vlv";
+  private static final String VLV_FILTER = "(objectClass=person)";
+
+  // @formatter:off
+  private static final User[] USERS = {
+    user(givenNames("Alice"),              surname("Zorro"),       employeeNumber(0)),
+    user(givenNames("Bob", "Robert"),      surname("deBuilder"),   employeeNumber(1)),
+    user(givenNames("Charlie"),            surname("Speed"),       employeeNumber(2)),
+    user(givenNames("Mickie"),             surname("Mouse"),       employeeNumber(3)),
+    user(givenNames("William", "Bill"),    surname("Beak"),        employeeNumber(4)),
+    user(givenNames(),                     surname("Prince"),      employeeNumber(5)),
+    user(givenNames("Charlie"),            surname("Chalk"),       employeeNumber(6)),
+    user(givenNames("Albert"),             surname("Einstein"),    employeeNumber(7)),
+    user(givenNames("Mini"),               surname("Mouse"),       employeeNumber(8)),
+  };
+  // @formatter:on
+
+  private static final int CONTENT_COUNT = USERS.length;
+
+  /** Indexed: ordered by ascending givenName then entryID */
+  private static final String SORT_ORDER_1 = "givenName";
+
+  /** Indexed: ordered by descending sn and descending employeeNumber then entryID */
+  private static final String SORT_ORDER_2 = "-sn -employeeNumber";
+
+  /** Unindexed: ordered by ascending sn and ascending employee number then entryID */
+  private static final String SORT_ORDER_3 = "sn employeeNumber";
+
+  /** Ordered by {@link #SORT_ORDER_1} */
+  private static final List<Integer> USERS_BY_ENTRY_ID = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8);
+
+  /** Indexed: ordered by {@link #SORT_ORDER_1} */
+  private static final List<Integer> USERS_BY_SORT_ORDER_1 = Arrays.asList(7, 0, 4, 1, 2, 6, 3, 8, 5);
+
+  /** Indexed: ordered by {@link #SORT_ORDER_2} */
+  private static final List<Integer> USERS_BY_SORT_ORDER_2 = Arrays.asList(0, 2, 5, 8, 3, 7, 1, 6, 4);
+
+  /** Unindexed: ordered by {@link #SORT_ORDER_3} */
+  private static final List<Integer> USERS_BY_SORT_ORDER_3 = Arrays.asList(4, 6, 1, 7, 3, 8, 5, 2, 0);
+
+  private PitBackend backend;
+
+  @BeforeClass
+  public void beforeClass() throws Exception
+  {
+    TestCaseUtils.startServer();
+
+    final DN baseDN = DN.valueOf(BACKEND_BASE_DN);
+
+    final PersistitBackendCfg backendCfg = mock(PersistitBackendCfg.class);
+    when(backendCfg.dn()).thenReturn(baseDN);
+    when(backendCfg.getBackendId()).thenReturn(BACKEND_NAME);
+    when(backendCfg.getBaseDN()).thenReturn(newSortedSet(baseDN));
+    when(backendCfg.listBackendIndexes()).thenReturn(new String[0]);
+    when(backendCfg.listBackendVLVIndexes()).thenReturn(new String[] { SORT_ORDER_1, SORT_ORDER_2 });
+    when(backendCfg.isSubordinateIndexesEnabled()).thenReturn(true);
+
+    when(backendCfg.getDBDirectory()).thenReturn(BACKEND_NAME);
+    when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
+    when(backendCfg.getDBCacheSize()).thenReturn(0L);
+    when(backendCfg.getDBCachePercent()).thenReturn(20);
+
+    createVlvIndex(baseDN, backendCfg, SORT_ORDER_1);
+    createVlvIndex(baseDN, backendCfg, SORT_ORDER_2);
+
+    backend = new PitBackend();
+    backend.setBackendID(backendCfg.getBackendId());
+    backend.configureBackend(backendCfg, DirectoryServer.getInstance().getServerContext());
+    backend.openBackend();
+
+    backend.addEntry(makeEntry("dn: " + BACKEND_BASE_DN, "objectclass: top", "objectclass: domain"), null);
+    for (final User user : USERS)
+    {
+      backend.addEntry(user.toEntry(), null);
+    }
+  }
+
+  private void createVlvIndex(final DN baseDN, final PersistitBackendCfg backendCfg, final String sortOrder)
+      throws ConfigException
+  {
+    final BackendVLVIndexCfg vlvIndexCfg = mock(BackendVLVIndexCfg.class);
+    when(vlvIndexCfg.getName()).thenReturn(sortOrder);
+    when(vlvIndexCfg.getBaseDN()).thenReturn(baseDN);
+    when(vlvIndexCfg.getFilter()).thenReturn(VLV_FILTER);
+    when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
+    when(vlvIndexCfg.getSortOrder()).thenReturn(sortOrder);
+    when(backendCfg.getBackendVLVIndex(sortOrder)).thenReturn(vlvIndexCfg);
+  }
+
+  @DataProvider
+  private Object[][] indexedVlvByAssertionDataProvider()
+  {
+    // @formatter:off
+    return new Object[][] {
+      {
+        SORT_ORDER_2,
+        beforeCount(0),
+        afterCount(5),
+        assertion("ZZ"),                                // before first
+        USERS_BY_SORT_ORDER_2.subList(0, 6),
+        expectedPosition(1)
+      },
+      {
+        SORT_ORDER_2,
+        beforeCount(0),
+        afterCount(5),
+        assertion("zorro"),                             // matches first
+        USERS_BY_SORT_ORDER_2.subList(0, 6),
+        expectedPosition(1)
+      },
+      {
+        SORT_ORDER_2,
+        beforeCount(0),
+        afterCount(3),
+        assertion("PRINCE"),                            // matches third
+        USERS_BY_SORT_ORDER_2.subList(2, 6),
+        expectedPosition(3)
+      },
+      {
+        SORT_ORDER_2,
+        beforeCount(1),
+        afterCount(3),
+        assertion("prince"),                            // matches third
+        USERS_BY_SORT_ORDER_2.subList(1, 6),
+        expectedPosition(3)
+      },
+      {
+        SORT_ORDER_2,
+        beforeCount(10),
+        afterCount(10),
+        assertion("prince"),                            // matches third
+        USERS_BY_SORT_ORDER_2,
+        expectedPosition(3)
+      },
+      {
+        SORT_ORDER_2,
+        beforeCount(0),
+        afterCount(3),
+        assertion("a"),                                 // after all
+        USERS_BY_SORT_ORDER_2.subList(9, 9),            // nothing
+        expectedPosition(10)
+      },
+    };
+    // @formatter:on
+  }
+
+  @Test(dataProvider = "indexedVlvByAssertionDataProvider")
+  public void indexedVlvByAssertionShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
+      final int beforeCount, final int afterCount, final String assertion, final List<Integer> expectedOrder,
+      final int expectedPosition) throws Exception
+  {
+    vlvByAssertion(sortOrder, beforeCount, afterCount, assertion, expectedOrder, expectedPosition);
+  }
+
+  @DataProvider
+  private Object[][] indexedVlvByOffsetDataProvider()
+  {
+    // @formatter:off
+    return new Object[][] {
+      {
+        SORT_ORDER_1,
+        beforeCount(0),
+        afterCount(3),
+        offset(1),
+        USERS_BY_SORT_ORDER_1.subList(0, 4)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(0),
+        afterCount(3),
+        offset(3),
+        USERS_BY_SORT_ORDER_1.subList(2, 6)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(0),
+        afterCount(0),
+        offset(5),
+        USERS_BY_SORT_ORDER_1.subList(4, 5)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(3),
+        afterCount(3),
+        offset(5),
+        USERS_BY_SORT_ORDER_1.subList(1, 8)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(0),
+        afterCount(3),
+        offset(0),                                      // 0 is equivalent to 1
+        USERS_BY_SORT_ORDER_1.subList(0, 4)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(3),                                 // underflow
+        afterCount(3),
+        offset(1),
+        USERS_BY_SORT_ORDER_1.subList(0, 4)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(3),
+        afterCount(3),
+        offset(30),                                     // overflow
+        USERS_BY_SORT_ORDER_1.subList(6, 9)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(3),
+        afterCount(3),                                  // overflow
+        offset(7),
+        USERS_BY_SORT_ORDER_1.subList(3, 9)
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(30),                                // underflow
+        afterCount(30),                                 // overflow
+        offset(7),
+        USERS_BY_SORT_ORDER_1                           // everything
+      },
+      {
+        SORT_ORDER_1,
+        beforeCount(0),                                 // underflow
+        afterCount(0),                                  // overflow
+        offset(30),
+        USERS_BY_SORT_ORDER_1.subList(9, 9)             //  nothing
+      },
+
+    };
+    // @formatter:on
+  }
+
+  @Test(dataProvider = "indexedVlvByOffsetDataProvider")
+  public void indexedVlvByOffsetShouldReturnPageOfResultsInCorrectOrder(final String sortOrder, final int beforeCount,
+      final int afterCount, final int offset, final List<Integer> expectedOrder) throws Exception
+  {
+    vlvByOffset(sortOrder, beforeCount, afterCount, offset, expectedOrder);
+  }
+
+  @Test
+  public void indexedVlvByOffsetShouldReturnOffsetRangeErrorForNegativeOffsets() throws Exception
+  {
+    final List<Control> responseControls = vlvByOffset0(SORT_ORDER_2, 0, 3, -1, USERS_BY_ENTRY_ID);
+    assertThat(responseControls).isNotEmpty();
+    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
+    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.OFFSET_RANGE_ERROR);
+  }
+
+  @DataProvider
+  private Object[][] unindexedVlvByAssertionDataProvider()
+  {
+    // @formatter:off
+    return new Object[][] {
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        assertion("a"),                                 // before first
+        USERS_BY_SORT_ORDER_3.subList(0, 4),
+        expectedPosition(1)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        assertion("beak"),                              // matches first
+        USERS_BY_SORT_ORDER_3.subList(0, 4),
+        expectedPosition(1)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        assertion("debuilder"),                         // matches third
+        USERS_BY_SORT_ORDER_3.subList(2, 6),
+        expectedPosition(3)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(1),
+        afterCount(3),
+        assertion("debuilder"),                         // matches third
+        USERS_BY_SORT_ORDER_3.subList(1, 6),
+        expectedPosition(3)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        assertion("zz"),                                // after all
+        USERS_BY_SORT_ORDER_3.subList(9, 9),            // nothing
+        expectedPosition(10)
+      },
+    };
+    // @formatter:on
+  }
+
+  @Test(dataProvider = "unindexedVlvByAssertionDataProvider")
+  public void unindexedVlvByAssertionShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
+      final int beforeCount, final int afterCount, final String assertion, final List<Integer> expectedOrder,
+      final int expectedPosition) throws Exception
+  {
+    vlvByAssertion(sortOrder, beforeCount, afterCount, assertion, expectedOrder, expectedPosition);
+  }
+
+  @DataProvider
+  private Object[][] unindexedVlvByOffsetDataProvider()
+  {
+    // @formatter:off
+    return new Object[][] {
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        offset(1),
+        USERS_BY_SORT_ORDER_3.subList(0, 4)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        offset(3),
+        USERS_BY_SORT_ORDER_3.subList(2, 6)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(0),
+        offset(5),
+        USERS_BY_SORT_ORDER_3.subList(4, 5)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(3),
+        afterCount(3),
+        offset(5),
+        USERS_BY_SORT_ORDER_3.subList(1, 8)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),
+        afterCount(3),
+        offset(0),                                      // 0 is equivalent to 1
+        USERS_BY_SORT_ORDER_3.subList(0, 4)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(3),                                 // underflow
+        afterCount(3),
+        offset(1),
+        USERS_BY_SORT_ORDER_3.subList(0, 4)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(3),
+        afterCount(3),
+        offset(30),                                     // overflow
+        USERS_BY_SORT_ORDER_3.subList(6, 9)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(3),
+        afterCount(3),                                  // overflow
+        offset(7),
+        USERS_BY_SORT_ORDER_3.subList(3, 9)
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(30),                                // underflow
+        afterCount(30),                                 // overflow
+        offset(7),
+        USERS_BY_SORT_ORDER_3                           // everything
+      },
+      {
+        SORT_ORDER_3,
+        beforeCount(0),                                 // underflow
+        afterCount(0),                                  // overflow
+        offset(30),
+        USERS_BY_SORT_ORDER_3.subList(9, 9)             //  nothing
+      },
+
+    };
+    // @formatter:on
+  }
+
+  @Test(dataProvider = "unindexedVlvByOffsetDataProvider")
+  public void unindexedVlvByOffsetShouldReturnPageOfResultsInCorrectOrder(final String sortOrder,
+      final int beforeCount, final int afterCount, final int offset, final List<Integer> expectedOrder)
+      throws Exception
+  {
+    vlvByOffset(sortOrder, beforeCount, afterCount, offset, expectedOrder);
+  }
+
+  @AfterClass
+  public void afterClass() throws Exception
+  {
+    backend.finalizeBackend();
+    backend = null;
+  }
+
+  private static int employeeNumber(final int i)
+  {
+    return i;
+  }
+
+  private static List<String> givenNames(final String... names)
+  {
+    return Arrays.asList(names);
+  }
+
+  private static String surname(final String name)
+  {
+    return name;
+  }
+
+  private static User user(final List<String> givenNames, final String sn, final int employeeNumber)
+  {
+    return new User(givenNames, sn, employeeNumber);
+  }
+
+  private int beforeCount(final int i)
+  {
+    return i;
+  }
+
+  private int afterCount(final int i)
+  {
+    return i;
+  }
+
+  private String assertion(final String s)
+  {
+    return s;
+  }
+
+  private int offset(final int i)
+  {
+    return i;
+  }
+
+  private int expectedPosition(final int i)
+  {
+    return i;
+  }
+
+  private ArrayList<DN> getDNs(final LinkedList<SearchResultEntry> entries)
+  {
+    final ArrayList<DN> results = new ArrayList<DN>();
+    for (final Entry e : entries)
+    {
+      results.add(e.getName());
+    }
+    return results;
+  }
+
+  private List<DN> getDNs(List<Integer> expectedOrder) throws Exception
+  {
+    List<DN> dns = new ArrayList<DN>(expectedOrder.size());
+    for (int i : expectedOrder)
+    {
+      dns.add(USERS[i].toDN());
+    }
+    return dns;
+  }
+
+  private ServerSideSortResponseControl getServerSideSortResponseControl(final Control c) throws DirectoryException
+  {
+    if (c instanceof LDAPControl)
+    {
+      return ServerSideSortResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl) c).getValue());
+    }
+    return (ServerSideSortResponseControl) c;
+  }
+
+  private ServerSideSortResponseControl getServerSideSortResponseControl(final List<Control> responseControls)
+      throws DirectoryException
+  {
+    for (final Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        return getServerSideSortResponseControl(c);
+      }
+    }
+    fail("Expected to find ServerSideSortResponseControl");
+    return null;
+  }
+
+  private VLVResponseControl getVLVResponseControl(final Control c) throws DirectoryException
+  {
+    if (c instanceof LDAPControl)
+    {
+      return VLVResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl) c).getValue());
+    }
+    return (VLVResponseControl) c;
+  }
+
+  private VLVResponseControl getVLVResponseControl(final List<Control> responseControls) throws DirectoryException
+  {
+    for (final Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        return getVLVResponseControl(c);
+      }
+    }
+    fail("Expected to find VLVResponseControl");
+    return null;
+  }
+
+  private void vlvByAssertion(final String sortOrder, final int beforeCount, final int afterCount,
+      final String assertion, final List<Integer> expectedOrder, final int expectedPosition) throws Exception
+  {
+    final SearchRequest request =
+        newSearchRequest(BACKEND_BASE_DN, SearchScope.WHOLE_SUBTREE, VLV_FILTER).addControl(
+            new ServerSideSortRequestControl(mangleSortOrder(sortOrder))).addControl(
+            new VLVRequestControl(beforeCount, afterCount, ByteString.valueOf(assertion)));
+    final InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
+    assertThat(internalSearch.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(getDNs(internalSearch.getSearchEntries())).isEqualTo(getDNs(expectedOrder));
+
+    final List<Control> responseControls = internalSearch.getResponseControls();
+    assertThat(responseControls).hasSize(2);
+
+    final ServerSideSortResponseControl sortResponse = getServerSideSortResponseControl(responseControls);
+    assertThat(sortResponse.getResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
+
+    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
+    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
+    assertThat(vlvResponse.getTargetPosition()).isEqualTo(expectedPosition);
+    assertThat(vlvResponse.getContentCount()).isEqualTo(CONTENT_COUNT);
+  }
+
+  private String mangleSortOrder(final String sortOrder)
+  {
+    return sortOrder.replaceAll(" ", ",");
+  }
+
+  private void vlvByOffset(final String sortOrder, final int beforeCount, final int afterCount, final int offset,
+      final List<Integer> expectedOrder) throws Exception, DirectoryException
+  {
+    final List<Control> responseControls = vlvByOffset0(sortOrder, beforeCount, afterCount, offset, expectedOrder);
+    assertThat(responseControls).hasSize(2);
+
+    final ServerSideSortResponseControl sortResponse = getServerSideSortResponseControl(responseControls);
+    assertThat(sortResponse.getResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
+
+    final VLVResponseControl vlvResponse = getVLVResponseControl(responseControls);
+    assertThat(vlvResponse.getVLVResultCode()).isEqualTo(LDAPResultCode.SUCCESS);
+    final int offsetInRange = Math.min(Math.max(offset, 1), CONTENT_COUNT + 1);
+    assertThat(vlvResponse.getTargetPosition()).isEqualTo(offsetInRange);
+    assertThat(vlvResponse.getContentCount()).isEqualTo(CONTENT_COUNT);
+  }
+
+  private List<Control> vlvByOffset0(final String sortOrder, final int beforeCount, final int afterCount,
+      final int offset, final List<Integer> expectedOrder) throws Exception
+  {
+    final SearchRequest request =
+        newSearchRequest(BACKEND_BASE_DN, SearchScope.WHOLE_SUBTREE, VLV_FILTER).addControl(
+            new ServerSideSortRequestControl(mangleSortOrder(sortOrder))).addControl(
+            new VLVRequestControl(beforeCount, afterCount, offset, 0));
+    final InternalSearchOperation internalSearch = getRootConnection().processSearch(request);
+
+    assertThat(internalSearch.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+    assertThat(getDNs(internalSearch.getSearchEntries())).isEqualTo(getDNs(expectedOrder));
+    return internalSearch.getResponseControls();
+  }
+
+  private static class User
+  {
+    private final int employeeNumber;
+    private final List<String> givenNames;
+    private final String sn;
+
+    private User(final List<String> givenNames, final String sn, final int employeeNumber)
+    {
+      this.givenNames = givenNames;
+      this.sn = sn;
+      this.employeeNumber = employeeNumber;
+    }
+
+    private DN toDN() throws Exception
+    {
+      return DN.valueOf(String.format("employeeNumber=%d,%s", employeeNumber, BACKEND_BASE_DN));
+    }
+
+    private Entry toEntry() throws Exception
+    {
+      final List<String> ldif = new ArrayList<String>();
+      ldif.add("dn: " + toDN());
+      ldif.add("objectClass: top");
+      ldif.add("objectClass: person");
+      ldif.add("objectClass: organizationalPerson");
+      ldif.add("objectClass: inetOrgPerson");
+      ldif.add(String.format("sn: %s", sn));
+      ldif.add(String.format("employeeNumber: %d", employeeNumber));
+      for (final String givenName : givenNames)
+      {
+        ldif.add(String.format("givenName: %s", givenName));
+      }
+      if (givenNames.isEmpty())
+      {
+        ldif.add(String.format("cn: %s", sn));
+      }
+      else
+      {
+        ldif.add(String.format("cn: %s %s", givenNames.get(0), sn));
+      }
+      return makeEntry(ldif.toArray(new String[0]));
+    }
+  }
+}

--
Gitblit v1.10.0