From 4b976b00d90e98ae2807d7bdc9efb79a4ce75751 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Mon, 30 Apr 2007 16:24:15 +0000
Subject: [PATCH] Implement support for the virtual list view (VLV) control as defined in draft-ietf-ldapext-ldapv3-vlv.  This can be used to retrieve a specified page of a search result set.  Any result set that can be used with server-side sorting can also be used with VLV.  The ldapsearch tool has also been updated to support this control.

---
 opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryContainer.java                         |   41 
 opendj-sdk/opends/src/server/org/opends/server/tools/LDAPSearch.java                                    |  135 ++
 opendj-sdk/opends/src/server/org/opends/server/controls/VLVRequestControl.java                          |  584 +++++++++++
 opendj-sdk/opends/src/server/org/opends/server/messages/JebMessages.java                                |   48 
 opendj-sdk/opends/src/server/org/opends/server/types/ResultCode.java                                    |   34 
 opendj-sdk/opends/src/server/org/opends/server/protocols/ldap/LDAPResultCode.java                       |   33 
 opendj-sdk/opends/src/server/org/opends/server/backends/jeb/BackendImpl.java                            |    1 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/LDAPSearchTestCase.java    |  265 +++++
 opendj-sdk/opends/src/server/org/opends/server/backends/jeb/SortValues.java                             |   35 
 opendj-sdk/opends/src/server/org/opends/server/messages/CoreMessages.java                               |   34 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/VLVControlTestCase.java | 1164 +++++++++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/controls/VLVResponseControl.java                         |  345 ++++++
 opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java                                |   16 
 opendj-sdk/opends/src/server/org/opends/server/messages/ToolMessages.java                               |  124 ++
 opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryIDSetSorter.java                       |  181 +++
 opendj-sdk/opends/src/server/org/opends/server/messages/ProtocolMessages.java                           |   98 +
 16 files changed, 3,129 insertions(+), 9 deletions(-)

diff --git a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/BackendImpl.java b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/BackendImpl.java
index 6088c48..6cd7658 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/BackendImpl.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/BackendImpl.java
@@ -111,6 +111,7 @@
     supportedControls.add(OID_PAGED_RESULTS_CONTROL);
     supportedControls.add(OID_MANAGE_DSAIT_CONTROL);
     supportedControls.add(OID_SERVER_SIDE_SORT_REQUEST_CONTROL);
+    supportedControls.add(OID_VLV_REQUEST_CONTROL);
   }
 
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryContainer.java b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryContainer.java
index fa8029f..18eb18b 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryContainer.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryContainer.java
@@ -42,6 +42,7 @@
 import org.opends.server.controls.PagedResultsControl;
 import org.opends.server.controls.ServerSideSortRequestControl;
 import org.opends.server.controls.ServerSideSortResponseControl;
+import org.opends.server.controls.VLVRequestControl;
 import org.opends.server.types.Attribute;
 import org.opends.server.types.AttributeType;
 import org.opends.server.types.AttributeValue;
@@ -552,6 +553,7 @@
     List<Control> controls = searchOperation.getRequestControls();
     PagedResultsControl pageRequest = null;
     ServerSideSortRequestControl sortRequest = null;
+    VLVRequestControl vlvRequest = null;
     if (controls != null)
     {
       for (Control control : controls)
@@ -575,6 +577,14 @@
               throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
                                            e.getMessage(), e.getMessageID(), e);
             }
+
+            if (vlvRequest != null)
+            {
+              int    msgID   = MSGID_JEB_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV;
+              String message = getMessage(msgID);
+              throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                           message, msgID);
+            }
           }
         }
         else if (control.getOID().equals(OID_SERVER_SIDE_SORT_REQUEST_CONTROL))
@@ -597,6 +607,34 @@
             }
           }
         }
+        else if (control.getOID().equals(OID_VLV_REQUEST_CONTROL))
+        {
+          // Ignore all but the first VLV request control.
+          if (vlvRequest == null)
+          {
+            try
+            {
+              vlvRequest = VLVRequestControl.decodeControl(control);
+            }
+            catch (LDAPException e)
+            {
+              if (debugEnabled())
+              {
+                debugCaught(DebugLogLevel.ERROR, e);
+              }
+              throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
+                                           e.getMessage(), e.getMessageID(), e);
+            }
+
+            if (pageRequest != null)
+            {
+              int    msgID   = MSGID_JEB_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV;
+              String message = getMessage(msgID);
+              throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
+                                           message, msgID);
+            }
+          }
+        }
       }
     }
 
@@ -759,7 +797,8 @@
         {
           entryIDList = EntryIDSetSorter.sort(this, entryIDList,
                                               searchOperation,
-                                              sortRequest.getSortOrder());
+                                              sortRequest.getSortOrder(),
+                                              vlvRequest);
           searchOperation.addResponseControl(
                new ServerSideSortResponseControl(LDAPResultCode.SUCCESS, null));
         }
diff --git a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryIDSetSorter.java b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryIDSetSorter.java
index 768b3b2..5b685a5 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryIDSetSorter.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/EntryIDSetSorter.java
@@ -28,13 +28,21 @@
 
 
 
+import java.util.Map;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.TreeMap;
 
+import org.opends.server.controls.VLVRequestControl;
+import org.opends.server.controls.VLVResponseControl;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.core.SearchOperation;
+import org.opends.server.protocols.ldap.LDAPResultCode;
+import org.opends.server.types.AttributeValue;
 import org.opends.server.types.DirectoryException;
 import org.opends.server.types.DN;
 import org.opends.server.types.Entry;
+import org.opends.server.types.ResultCode;
 import org.opends.server.types.SearchFilter;
 import org.opends.server.types.SearchScope;
 import org.opends.server.types.SortOrder;
@@ -60,6 +68,8 @@
    * @param  entryIDSet       The entry ID set to be sorted.
    * @param  searchOperation  The search operation being processed.
    * @param  sortOrder        The sort order to use for the entry ID set.
+   * @param  vlvRequest       The VLV request control included in the search
+   *                          request, or {@code null} if there was none.
    *
    * @return  A new entry ID set which is a sorted representation of the
    *          provided set using the given sort order.
@@ -69,7 +79,8 @@
   public static EntryIDSet sort(EntryContainer entryContainer,
                                 EntryIDSet entryIDSet,
                                 SearchOperation searchOperation,
-                                SortOrder sortOrder)
+                                SortOrder sortOrder,
+                                VLVRequestControl vlvRequest)
          throws DirectoryException
   {
     if (! entryIDSet.isDefined())
@@ -107,11 +118,171 @@
       }
     }
 
-    long[] sortedIDs = new long[sortMap.size()];
-    int i=0;
-    for (EntryID id : sortMap.values())
+
+    // See if there is a VLV request to further pare down the set of results,
+    // and if there is where it should be processed by offset or assertion
+    // value.
+    long[] sortedIDs;
+    if (vlvRequest != null)
     {
-      sortedIDs[i++] = id.longValue();
+      int beforeCount = vlvRequest.getBeforeCount();
+      int afterCount  = vlvRequest.getAfterCount();
+
+      if (vlvRequest.getTargetType() == VLVRequestControl.TYPE_TARGET_BYOFFSET)
+      {
+        int targetOffset = vlvRequest.getOffset();
+        int listOffset = targetOffset - 1; // VLV offsets start at 1, not 0.
+        int startPos = listOffset - beforeCount;
+        if (startPos < 0)
+        {
+          searchOperation.addResponseControl(
+               new VLVResponseControl(targetOffset, sortMap.size(),
+                                      LDAPResultCode.OFFSET_RANGE_ERROR));
+
+          int    msgID   = MSGID_ENTRYIDSORTER_NEGATIVE_START_POS;
+          String message = getMessage(msgID);
+          throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR,
+                                       message, msgID);
+        }
+        else if (startPos >= sortMap.size())
+        {
+          searchOperation.addResponseControl(
+               new VLVResponseControl(targetOffset, sortMap.size(),
+                                      LDAPResultCode.OFFSET_RANGE_ERROR));
+
+          int    msgID   = MSGID_ENTRYIDSORTER_OFFSET_TOO_LARGE;
+          String message = getMessage(msgID, vlvRequest.getOffset(),
+                                      sortMap.size());
+          throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR,
+                                       message, msgID);
+        }
+
+        int count = 1 + beforeCount + afterCount;
+        sortedIDs = new long[count];
+
+        int treePos = 0;
+        int arrayPos = 0;
+        Iterator<EntryID> idIterator = sortMap.values().iterator();
+        while (idIterator.hasNext())
+        {
+          EntryID id = idIterator.next();
+          if (treePos++ < startPos)
+          {
+            continue;
+          }
+
+          sortedIDs[arrayPos++] = id.longValue();
+          if (arrayPos >= count)
+          {
+            break;
+          }
+        }
+
+        if (arrayPos < count)
+        {
+          // We don't have enough entries in the set to meet the requested
+          // page size, so we'll need to shorten the array.
+          long[] newIDArray = new long[arrayPos];
+          System.arraycopy(sortedIDs, 0, newIDArray, 0, arrayPos);
+          sortedIDs = newIDArray;
+        }
+
+        searchOperation.addResponseControl(
+             new VLVResponseControl(targetOffset, sortMap.size(),
+                                    LDAPResultCode.SUCCESS));
+      }
+      else
+      {
+        AttributeValue assertionValue = new
+             AttributeValue(sortOrder.getSortKeys()[0].getAttributeType(),
+                            vlvRequest.getGreaterThanOrEqualAssertion());
+
+        boolean targetFound     = false;
+        int targetOffset        = 0;
+        int includedBeforeCount = 0;
+        int includedAfterCount  = 0;
+        int listSize            = 0;
+        LinkedList<EntryID> idList = new LinkedList<EntryID>();
+        Iterator<Map.Entry<SortValues,EntryID>> mapIterator =
+             sortMap.entrySet().iterator();
+        while (mapIterator.hasNext())
+        {
+          Map.Entry<SortValues,EntryID> entry = mapIterator.next();
+          SortValues sortValues = entry.getKey();
+          EntryID id = entry.getValue();
+
+          if (targetFound)
+          {
+            idList.add(id);
+            listSize++;
+            includedAfterCount++;
+            if (includedAfterCount >= afterCount)
+            {
+              break;
+            }
+          }
+          else
+          {
+            targetFound = (sortValues.compareTo(assertionValue) >= 0);
+            targetOffset++;
+
+            if (targetFound)
+            {
+              idList.add(id);
+              listSize++;
+            }
+            else if (beforeCount > 0)
+            {
+              if (beforeCount > 0)
+              {
+                idList.add(id);
+                includedBeforeCount++;
+                if (includedBeforeCount > beforeCount)
+                {
+                  idList.removeFirst();
+                  includedBeforeCount--;
+                }
+                else
+                {
+                  listSize++;
+                }
+              }
+            }
+          }
+        }
+
+        if (! targetFound)
+        {
+          searchOperation.addResponseControl(
+               new VLVResponseControl(sortMap.size(), sortMap.size(),
+                                      LDAPResultCode.OFFSET_RANGE_ERROR));
+
+          int    msgID   = MSGID_ENTRYIDSORTER_TARGET_VALUE_NOT_FOUND;
+          String message = getMessage(msgID);
+          throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR,
+                                       message, msgID);
+        }
+
+        sortedIDs = new long[listSize];
+        Iterator<EntryID> idIterator = idList.iterator();
+        for (int i=0; i < listSize; i++)
+        {
+          sortedIDs[i] = idIterator.next().longValue();
+        }
+
+        searchOperation.addResponseControl(
+             new VLVResponseControl(targetOffset, sortMap.size(),
+                                    LDAPResultCode.SUCCESS));
+      }
+    }
+    else
+    {
+      sortedIDs = new long[sortMap.size()];
+      int i=0;
+      for (EntryID id : sortMap.values())
+      {
+        sortedIDs[i++] = id.longValue();
+      }
     }
 
     return new EntryIDSet(sortedIDs, 0, sortedIDs.length);
diff --git a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/SortValues.java b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/SortValues.java
index 2ee7f8d..1d26ff8 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/SortValues.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/backends/jeb/SortValues.java
@@ -165,6 +165,28 @@
 
 
   /**
+   * Compares the first element in this set of sort values with the provided
+   * assertion value to determine whether the assertion value is greater than or
+   * equal to the initial sort value.  This is used during VLV processing to
+   * find the offset by assertion value.
+   *
+   * @param  assertionValue  The assertion value to compare against the first
+   *                         sort value.
+   *
+   * @return  A negative value if the provided assertion value should come
+   *          before the first sort value, zero if the provided assertion value
+   *          is equal to the first sort value, or a positive value if the
+   *          provided assertion value should come after the first sort value.
+   */
+  public int compareTo(AttributeValue assertionValue)
+  {
+    SortKey sortKey = sortOrder.getSortKeys()[0];
+    return sortKey.compareValues(values[0], assertionValue);
+  }
+
+
+
+  /**
    * Retrieves a string representation of this sort values object.
    *
    * @return  A string representation of this sort values object.
@@ -193,7 +215,7 @@
     {
       if (i > 0)
       {
-        buffer.append(", ");
+        buffer.append(",");
       }
 
       if (sortKeys[i].ascending())
@@ -207,9 +229,18 @@
 
       buffer.append(sortKeys[i].getAttributeType().getNameOrOID());
       buffer.append("=");
-      buffer.append(values[i].getStringValue());
+      if (values[i] == null)
+      {
+        buffer.append("null");
+      }
+      else
+      {
+        buffer.append(values[i].getStringValue());
+      }
     }
 
+    buffer.append(", id=");
+    buffer.append(entryID.toString());
     buffer.append(")");
   }
 }
diff --git a/opendj-sdk/opends/src/server/org/opends/server/controls/VLVRequestControl.java b/opendj-sdk/opends/src/server/org/opends/server/controls/VLVRequestControl.java
new file mode 100644
index 0000000..1e0ff6f
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/controls/VLVRequestControl.java
@@ -0,0 +1,584 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.controls;
+
+
+
+import java.util.ArrayList;
+
+import org.opends.server.protocols.asn1.ASN1Element;
+import org.opends.server.protocols.asn1.ASN1Integer;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.asn1.ASN1Sequence;
+import org.opends.server.protocols.ldap.LDAPResultCode;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.Control;
+import org.opends.server.types.LDAPException;
+
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.messages.ProtocolMessages.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class implements the virtual list view request controls as defined in
+ * draft-ietf-ldapext-ldapv3-vlv.  The ASN.1 description for the control value
+ * is:
+ * <BR><BR>
+ * <PRE>
+ * VirtualListViewRequest ::= SEQUENCE {
+ *       beforeCount    INTEGER (0..maxInt),
+ *       afterCount     INTEGER (0..maxInt),
+ *       target       CHOICE {
+ *                      byOffset        [0] SEQUENCE {
+ *                           offset          INTEGER (1 .. maxInt),
+ *                           contentCount    INTEGER (0 .. maxInt) },
+ *                      greaterThanOrEqual [1] AssertionValue },
+ *       contextID     OCTET STRING OPTIONAL }
+ * </PRE>
+ */
+public class VLVRequestControl
+       extends Control
+{
+  /**
+   * The BER type to use when encoding the byOffset target element.
+   */
+  public static final byte TYPE_TARGET_BYOFFSET = (byte) 0xA0;
+
+
+
+  /**
+   * The BER type to use when encoding the greaterThanOrEqual target element.
+   */
+  public static final byte TYPE_TARGET_GREATERTHANOREQUAL = (byte) 0x81;
+
+
+
+  // The target type for this VLV request control.
+  private byte targetType;
+
+  // The context ID for this VLV request control.
+  private ByteString contextID;
+
+  // The greaterThanOrEqual target assertion value for this VLV request control.
+  private ByteString greaterThanOrEqual;
+
+  // The after count for this VLV request control.
+  private int afterCount;
+
+  // The before count for this VLV request control.
+  private int beforeCount;
+
+  // The content count for the byOffset target of this VLV request control.
+  private int contentCount;
+
+  // The offset for the byOffset target of this VLV request control.
+  private int offset;
+
+
+
+  /**
+   * Creates a new VLV request control with the provided information.
+   *
+   * @param  beforeCount   The number of entries before the target offset to
+   *                       retrieve in the results page.
+   * @param  afterCount    The number of entries after the target offset to
+   *                       retrieve in the results page.
+   * @param  offset        The offset in the result set to target for the
+   *                       beginning of the page of results.
+   * @param  contentCount  The content count returned by the server in the last
+   *                       phase of the VLV request, or zero for a new VLV
+   *                       request session.
+   */
+  public VLVRequestControl(int beforeCount, int afterCount, int offset,
+                           int contentCount)
+  {
+    this(beforeCount, afterCount, offset, contentCount, null);
+  }
+
+
+
+  /**
+   * Creates a new VLV request control with the provided information.
+   *
+   * @param  beforeCount   The number of entries before the target offset to
+   *                       retrieve in the results page.
+   * @param  afterCount    The number of entries after the target offset to
+   *                       retrieve in the results page.
+   * @param  offset        The offset in the result set to target for the
+   *                       beginning of the page of results.
+   * @param  contentCount  The content count returned by the server in the last
+   *                       phase of the VLV request, or zero for a new VLV
+   *                       request session.
+   * @param  contextID     The context ID provided by the server in the last
+   *                       VLV response for the same set of criteria, or
+   *                       {@code null} if there was no previous VLV response or
+   *                       the server did not include a context ID in the
+   *                       last response.
+   */
+  public VLVRequestControl(int beforeCount, int afterCount, int offset,
+                           int contentCount, ByteString contextID)
+  {
+    super(OID_VLV_REQUEST_CONTROL, false,
+          encodeControlValue(beforeCount, afterCount, offset, contentCount,
+                             contextID));
+
+    this.beforeCount  = beforeCount;
+    this.afterCount   = afterCount;
+    this.offset       = offset;
+    this.contentCount = contentCount;
+    this.contextID    = contextID;
+
+    targetType = TYPE_TARGET_BYOFFSET;
+  }
+
+
+
+  /**
+   * Creates a new VLV request control with the provided information.
+   *
+   * @param  beforeCount         The number of entries before the target offset
+   *                             to retrieve in the results page.
+   * @param  afterCount          The number of entries after the target offset
+   *                             to retrieve in the results page.
+   * @param  greaterThanOrEqual  The greaterThanOrEqual target assertion value
+   *                             that indicates where to start the page of
+   *                             results.
+   */
+  public VLVRequestControl(int beforeCount, int afterCount,
+                           ByteString greaterThanOrEqual)
+  {
+    this(beforeCount, afterCount, greaterThanOrEqual, null);
+  }
+
+
+
+  /**
+   * Creates a new VLV request control with the provided information.
+   *
+   * @param  beforeCount         The number of entries before the target
+   *                             assertion value.
+   * @param  afterCount          The number of entries after the target
+   *                             assertion value.
+   * @param  greaterThanOrEqual  The greaterThanOrEqual target assertion value
+   *                             that indicates where to start the page of
+   *                             results.
+   * @param  contextID           The context ID provided by the server in the
+   *                             last VLV response for the same set of criteria,
+   *                             or {@code null} if there was no previous VLV
+   *                             response or the server did not include a
+   *                             context ID in the last response.
+   */
+  public VLVRequestControl(int beforeCount, int afterCount,
+                           ByteString greaterThanOrEqual,
+                           ByteString contextID)
+  {
+    super(OID_VLV_REQUEST_CONTROL, false,
+          encodeControlValue(beforeCount, afterCount, greaterThanOrEqual,
+                             contextID));
+
+    this.beforeCount        = beforeCount;
+    this.afterCount         = afterCount;
+    this.greaterThanOrEqual = greaterThanOrEqual;
+    this.contextID          = contextID;
+
+    targetType = TYPE_TARGET_GREATERTHANOREQUAL;
+  }
+
+
+
+  /**
+   * Creates a new VLV request control with the provided information.
+   *
+   * @param  oid                 The OID for the control.
+   * @param  isCritical          Indicates whether the control should be
+   *                             considered critical.
+   * @param  controlValue        The pre-encoded value for the control.
+   * @param  beforeCount         The number of entries before the target
+   *                             assertion value.
+   * @param  afterCount          The number of entries after the target
+   *                             assertion value.
+   * @param  greaterThanOrEqual  The greaterThanOrEqual target assertion value
+   *                             that indicates where to start the page of
+   *                             results.
+   * @param  contextID           The context ID provided by the server in the
+   *                             last VLV response for the same set of criteria,
+   *                             or {@code null} if there was no previous VLV
+   *                             response or the server did not include a
+   *                             context ID in the last response.
+   */
+  private VLVRequestControl(String oid, boolean isCritical,
+                            ASN1OctetString controlValue, int beforeCount,
+                            int afterCount, byte targetType,
+                            int offset, int contentCount,
+                            ByteString greaterThanOrEqual,
+                            ByteString contextID)
+  {
+    super(oid, isCritical, controlValue);
+
+    this.beforeCount        = beforeCount;
+    this.afterCount         = afterCount;
+    this.targetType         = targetType;
+    this.offset             = offset;
+    this.contentCount       = contentCount;
+    this.greaterThanOrEqual = greaterThanOrEqual;
+    this.contextID          = contextID;
+  }
+
+
+
+  /**
+   * Retrieves the number of entries before the target offset or assertion value
+   * to include in the results page.
+   *
+   * @return  The number of entries before the target offset to include in the
+   *          results page.
+   */
+  public int getBeforeCount()
+  {
+    return beforeCount;
+  }
+
+
+
+  /**
+   * Retrieves the number of entries after the target offset or assertion value
+   * to include in the results page.
+   *
+   * @return  The number of entries after the target offset to include in the
+   *          results page.
+   */
+  public int getAfterCount()
+  {
+    return afterCount;
+  }
+
+
+
+  /**
+   * Retrieves the BER type for the target that specifies the beginning of the
+   * results page.
+   *
+   * @return  {@code TYPE_TARGET_BYOFFSET} if the beginning of the results page
+   *          should be specified as a nuemric offset, or
+   *          {@code TYPE_TARGET_GREATERTHANOREQUAL} if it should be specified
+   *          by an assertion value.
+   */
+  public byte getTargetType()
+  {
+    return targetType;
+  }
+
+
+
+  /**
+   * Retrieves the offset that indicates the beginning of the results page.  The
+   * return value will only be applicable if the {@code getTargetType} method
+   * returns {@code TYPE_TARGET_BYOFFSET}.
+   *
+   * @return  The offset that indicates the beginning of the results page.
+   */
+  public int getOffset()
+  {
+    return offset;
+  }
+
+
+
+  /**
+   * Retrieves the content count indicating the estimated number of entries in
+   * the complete result set.  The return value will only be applicable if the
+   * {@code getTargetType} method returns {@code TYPE_TARGET_BYOFFSET}.
+   *
+   * @return  The content count indicating the estimated number of entries in
+   *          the complete result set.
+   */
+  public int getContentCount()
+  {
+    return contentCount;
+  }
+
+
+
+  /**
+   * Retrieves the assertion value that will be used to locate the beginning of
+   * the results page.  This will only be applicable if the
+   * {@code getTargetType} method returns
+   * {@code TYPE_TARGET_GREATERTHANOREQUAL}.
+   *
+   * @return  The assertion value that will be used to locate the beginning of
+   *          the results page, or {@code null} if the beginning of the results
+   *          page is to be specified using an offset.
+   */
+  public ByteString getGreaterThanOrEqualAssertion()
+  {
+    return greaterThanOrEqual;
+  }
+
+
+
+  /**
+   * Retrieves a context ID value that should be used to resume a previous VLV
+   * results session.
+   *
+   * @return  A context ID value that should be used to resume a previous VLV
+   *          results session, or {@code null} if none is available.
+   */
+  public ByteString getContextID()
+  {
+    return contextID;
+  }
+
+
+
+  /**
+   * Encodes the provided information in a manner suitable for use as the value
+   * of this control.
+   *
+   * @param  beforeCount   The number of entries before the target offset to
+   *                       retrieve in the results page.
+   * @param  afterCount    The number of entries after the target offset to
+   *                       retrieve in the results page.
+   * @param  offset        The offset in the result set to target for the
+   *                       beginning of the page of results.
+   * @param  contentCount  The content count returned by the server in the last
+   *                       phase of the VLV request, or zero for a new VLV
+   *                       request session.
+   * @param  contextID     The context ID provided by the server in the last
+   *                       VLV response for the same set of criteria, or
+   *                       {@code null} if there was no previous VLV response or
+   *                       the server did not include a context ID in the
+   *                       last response.
+   *
+   * @return  The ASN.1 octet string containing the encoded sort order.
+   */
+  private static ASN1OctetString encodeControlValue(int beforeCount,
+                                      int afterCount, int offset,
+                                      int contentCount, ByteString contextID)
+  {
+    ArrayList<ASN1Element> vlvElements = new ArrayList<ASN1Element>(4);
+    vlvElements.add(new ASN1Integer(beforeCount));
+    vlvElements.add(new ASN1Integer(afterCount));
+
+    ArrayList<ASN1Element> targetElements = new ArrayList<ASN1Element>(2);
+    targetElements.add(new ASN1Integer(offset));
+    targetElements.add(new ASN1Integer(contentCount));
+    vlvElements.add(new ASN1Sequence(TYPE_TARGET_BYOFFSET, targetElements));
+
+    if (contextID != null)
+    {
+      vlvElements.add(contextID.toASN1OctetString());
+    }
+
+    return new ASN1OctetString(new ASN1Sequence(vlvElements).encode());
+  }
+
+
+
+  /**
+   * Encodes the provided information in a manner suitable for use as the value
+   * of this control.
+   *
+   * @param  beforeCount         The number of entries before the target
+   *                             assertion value.
+   * @param  afterCount          The number of entries after the target
+   *                             assertion value.
+   * @param  greaterThanOrEqual  The greaterThanOrEqual target assertion value
+   *                             that indicates where to start the page of
+   *                             results.
+   * @param  contextID           The context ID provided by the server in the
+   *                             last VLV response for the same set of criteria,
+   *                             or {@code null} if there was no previous VLV
+   *                             response or the server did not include a
+   *                             context ID in the last response.
+   *
+   * @return  The ASN.1 octet string containing the encoded sort order.
+   */
+  private static ASN1OctetString encodeControlValue(int beforeCount,
+                                      int afterCount,
+                                      ByteString greaterThanOrEqual,
+                                      ByteString contextID)
+  {
+    ArrayList<ASN1Element> vlvElements = new ArrayList<ASN1Element>(4);
+    vlvElements.add(new ASN1Integer(beforeCount));
+    vlvElements.add(new ASN1Integer(afterCount));
+
+    vlvElements.add(new ASN1OctetString(TYPE_TARGET_GREATERTHANOREQUAL,
+                                        greaterThanOrEqual.value()));
+
+    if (contextID != null)
+    {
+      vlvElements.add(contextID.toASN1OctetString());
+    }
+
+    return new ASN1OctetString(new ASN1Sequence(vlvElements).encode());
+  }
+
+
+
+  /**
+   * Creates a new VLV request control from the contents of the provided
+   * control.
+   *
+   * @param  control  The generic control containing the information to use to
+   *                  create this VLV request control.  It must not be
+   *                  {@code null}.
+   *
+   * @return  The VLV request control decoded from the provided control.
+   *
+   * @throws  LDAPException  If this control cannot be decoded as a valid VLV
+   *                         request control.
+   */
+  public static VLVRequestControl decodeControl(Control control)
+         throws LDAPException
+  {
+    ASN1OctetString controlValue = control.getValue();
+    if (controlValue == null)
+    {
+      int    msgID   = MSGID_VLVREQ_CONTROL_NO_VALUE;
+      String message = getMessage(msgID);
+      throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message);
+    }
+
+    try
+    {
+      ASN1Sequence vlvSequence =
+           ASN1Sequence.decodeAsSequence(controlValue.value());
+      ArrayList<ASN1Element> elements = vlvSequence.elements();
+
+      if ((elements.size() < 3) || (elements.size() > 4))
+      {
+        int    msgID   = MSGID_VLVREQ_CONTROL_INVALID_ELEMENT_COUNT;
+        String message = getMessage(msgID, elements.size());
+        throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message);
+      }
+
+      int beforeCount = elements.get(0).decodeAsInteger().intValue();
+      int afterCount  = elements.get(1).decodeAsInteger().intValue();
+
+      ASN1Element targetElement = elements.get(2);
+      int offset = 0;
+      int contentCount = 0;
+      ASN1OctetString greaterThanOrEqual = null;
+      byte targetType = targetElement.getType();
+      switch (targetType)
+      {
+        case TYPE_TARGET_BYOFFSET:
+          ArrayList<ASN1Element> targetElements =
+               targetElement.decodeAsSequence().elements();
+          offset = targetElements.get(0).decodeAsInteger().intValue();
+          contentCount = targetElements.get(1).decodeAsInteger().intValue();
+          break;
+
+        case TYPE_TARGET_GREATERTHANOREQUAL:
+          greaterThanOrEqual = targetElement.decodeAsOctetString();
+          break;
+
+        default:
+          int    msgID   = MSGID_VLVREQ_CONTROL_INVALID_TARGET_TYPE;
+          String message = getMessage(msgID, byteToHex(targetType));
+          throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID,
+                                  message);
+      }
+
+      ASN1OctetString contextID = null;
+      if (elements.size() == 4)
+      {
+        contextID = elements.get(3).decodeAsOctetString();
+      }
+
+      return new VLVRequestControl(control.getOID(), control.isCritical(),
+                                   controlValue, beforeCount, afterCount,
+                                   targetType, offset, contentCount,
+                                   greaterThanOrEqual, contextID);
+    }
+    catch (LDAPException le)
+    {
+      throw le;
+    }
+    catch (Exception e)
+    {
+      int    msgID   = MSGID_VLVREQ_CONTROL_CANNOT_DECODE_VALUE;
+      String message = getMessage(msgID, getExceptionMessage(e));
+      throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message, e);
+    }
+  }
+
+
+
+  /**
+   * Retrieves a string representation of this VLV request control.
+   *
+   * @return  A string representation of this VLV request control.
+   */
+  public String toString()
+  {
+    StringBuilder buffer = new StringBuilder();
+    toString(buffer);
+    return buffer.toString();
+  }
+
+
+
+  /**
+   * Appends a string representation of this VLV request control to the provided
+   * buffer.
+   *
+   * @param  buffer  The buffer to which the information should be appended.
+   */
+  public void toString(StringBuilder buffer)
+  {
+    buffer.append("VLVRequestControl(beforeCount=");
+    buffer.append(beforeCount);
+    buffer.append(", afterCount=");
+    buffer.append(afterCount);
+
+    if (targetType == TYPE_TARGET_BYOFFSET)
+    {
+      buffer.append(", offset=");
+      buffer.append(offset);
+      buffer.append(", contentCount=");
+      buffer.append(contentCount);
+    }
+    else
+    {
+      buffer.append(", greaterThanOrEqual=");
+      buffer.append(greaterThanOrEqual);
+    }
+
+    if (contextID != null)
+    {
+      buffer.append(", contextID=");
+      buffer.append(contextID);
+    }
+
+    buffer.append(")");
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/controls/VLVResponseControl.java b/opendj-sdk/opends/src/server/org/opends/server/controls/VLVResponseControl.java
new file mode 100644
index 0000000..2960500
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/controls/VLVResponseControl.java
@@ -0,0 +1,345 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.controls;
+
+
+
+import java.util.ArrayList;
+
+import org.opends.server.protocols.asn1.ASN1Element;
+import org.opends.server.protocols.asn1.ASN1Enumerated;
+import org.opends.server.protocols.asn1.ASN1Integer;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.asn1.ASN1Sequence;
+import org.opends.server.protocols.ldap.LDAPResultCode;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.Control;
+import org.opends.server.types.LDAPException;
+
+import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.messages.ProtocolMessages.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+
+
+
+/**
+ * This class implements the virtual list view response controls as defined in
+ * draft-ietf-ldapext-ldapv3-vlv.  The ASN.1 description for the control value
+ * is:
+ * <BR><BR>
+ * <PRE>
+ * VirtualListViewResponse ::= SEQUENCE {
+ *       targetPosition    INTEGER (0 .. maxInt),
+ *       contentCount     INTEGER (0 .. maxInt),
+ *       virtualListViewResult ENUMERATED {
+ *            success (0),
+ *            operationsError (1),
+ *            protocolError (3),
+ *            unwillingToPerform (53),
+ *            insufficientAccessRights (50),
+ *            timeLimitExceeded (3),
+ *            adminLimitExceeded (11),
+ *            innapropriateMatching (18),
+ *            sortControlMissing (60),
+ *            offsetRangeError (61),
+ *            other(80),
+ *            ... },
+ *       contextID     OCTET STRING OPTIONAL }
+ * </PRE>
+ */
+public class VLVResponseControl
+       extends Control
+{
+  // The context ID for this VLV response control.
+  private ByteString contextID;
+
+  // The content count estimating the total number of entries in the result set.
+  private int contentCount;
+
+  // The offset of the target entry in the result set.
+  private int targetPosition;
+
+  // The result code for the VLV operation.
+  private int vlvResultCode;
+
+
+
+  /**
+   * Creates a new VLV response control with the provided information.
+   *
+   * @param  targetPosition  The position of the target entry in the result set.
+   * @param  contentCount    The content count estimating the total number of
+   *                         entries in the result set.
+   * @param  vlvResultCode   The result code for the VLV operation.
+   */
+  public VLVResponseControl(int targetPosition, int contentCount,
+                            int vlvResultCode)
+  {
+    this(targetPosition, contentCount, vlvResultCode, null);
+  }
+
+
+
+  /**
+   * Creates a new VLV response control with the provided information.
+   *
+   * @param  targetPosition  The position of the target entry in the result set.
+   * @param  contentCount    The content count estimating the total number of
+   *                         entries in the result set.
+   * @param  vlvResultCode   The result code for the VLV operation.
+   * @param  contextID       The context ID for this VLV response control.
+   */
+  public VLVResponseControl(int targetPosition, int contentCount,
+                            int vlvResultCode, ByteString contextID)
+  {
+    super(OID_VLV_RESPONSE_CONTROL, false,
+          encodeControlValue(targetPosition, contentCount, vlvResultCode,
+                             contextID));
+
+    this.targetPosition = targetPosition;
+    this.contentCount   = contentCount;
+    this.vlvResultCode  = vlvResultCode;
+    this.contextID      = contextID;
+  }
+
+
+
+  /**
+   * Creates a new VLV response control with the provided information.
+   *
+   * @param  oid             The OID for the control.
+   * @param  isCritical      Indicates whether the control should be considered
+   *                         critical.
+   * @param  controlValue    The pre-encoded value for the control.
+   * @param  targetPosition  The position of the target entry in the result set.
+   * @param  contentCount    The content count estimating the total number of
+   *                         entries in the result set.
+   * @param  vlvResultCode   The result code for the VLV operation.
+   * @param  contextID       The context ID for this VLV response control.
+   */
+  private VLVResponseControl(String oid, boolean isCritical,
+                             ASN1OctetString controlValue, int targetPosition,
+                             int contentCount, int vlvResultCode,
+                             ByteString contextID)
+  {
+    super(oid, isCritical, controlValue);
+
+    this.targetPosition = targetPosition;
+    this.contentCount   = contentCount;
+    this.vlvResultCode  = vlvResultCode;
+    this.contextID      = contextID;
+  }
+
+
+
+  /**
+   * Retrieves the position of the target entry in the result set.
+   *
+   * @return  The position of the target entry in the result set.
+   */
+  public int getTargetPosition()
+  {
+    return targetPosition;
+  }
+
+
+
+  /**
+   * Retrieves the estimated total number of entries in the result set.
+   *
+   * @return  The estimated total number of entries in the result set.
+   */
+  public int getContentCount()
+  {
+    return contentCount;
+  }
+
+
+
+  /**
+   * Retrieves the result code for the VLV operation.
+   *
+   * @return  The result code for the VLV operation.
+   */
+  public int getVLVResultCode()
+  {
+    return vlvResultCode;
+  }
+
+
+
+  /**
+   * Retrieves a context ID value that should be included in the next request
+   * to retrieve a page of the same result set.
+   *
+   * @return  A context ID value that should be included in the next request to
+   *          retrieve a page of the same result set, or {@code null} if there
+   *          is no context ID.
+   */
+  public ByteString getContextID()
+  {
+    return contextID;
+  }
+
+
+
+  /**
+   * Encodes the provided information in a manner suitable for use as the value
+   * of this control.
+   *
+   * @param  targetPosition  The position of the target entry in the result set.
+   * @param  contentCount    The content count estimating the total number of
+   *                         entries in the result set.
+   * @param  vlvResultCode   The result code for the VLV operation.
+   * @param  contextID       The context ID for this VLV response control.
+   *
+   * @return  The ASN.1 octet string containing the encoded sort order.
+   */
+  private static ASN1OctetString encodeControlValue(int targetPosition,
+                                      int contentCount, int vlvResultCode,
+                                      ByteString contextID)
+  {
+    ArrayList<ASN1Element> vlvElements = new ArrayList<ASN1Element>(4);
+    vlvElements.add(new ASN1Integer(targetPosition));
+    vlvElements.add(new ASN1Integer(contentCount));
+    vlvElements.add(new ASN1Enumerated(vlvResultCode));
+
+    if (contextID != null)
+    {
+      vlvElements.add(contextID.toASN1OctetString());
+    }
+
+    return new ASN1OctetString(new ASN1Sequence(vlvElements).encode());
+  }
+
+
+
+  /**
+   * Creates a new VLV response control from the contents of the provided
+   * control.
+   *
+   * @param  control  The generic control containing the information to use to
+   *                  create this VLV response control.  It must not be
+   *                  {@code null}.
+   *
+   * @return  The VLV response control decoded from the provided control.
+   *
+   * @throws  LDAPException  If this control cannot be decoded as a valid VLV
+   *                         response control.
+   */
+  public static VLVResponseControl decodeControl(Control control)
+         throws LDAPException
+  {
+    ASN1OctetString controlValue = control.getValue();
+    if (controlValue == null)
+    {
+      int    msgID   = MSGID_VLVRES_CONTROL_NO_VALUE;
+      String message = getMessage(msgID);
+      throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message);
+    }
+
+    try
+    {
+      ASN1Sequence vlvSequence =
+           ASN1Sequence.decodeAsSequence(controlValue.value());
+      ArrayList<ASN1Element> elements = vlvSequence.elements();
+
+      if ((elements.size() < 3) || (elements.size() > 4))
+      {
+        int    msgID   = MSGID_VLVRES_CONTROL_INVALID_ELEMENT_COUNT;
+        String message = getMessage(msgID, elements.size());
+        throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message);
+      }
+
+      int targetPosition = elements.get(0).decodeAsInteger().intValue();
+      int contentCount   = elements.get(1).decodeAsInteger().intValue();
+      int vlvResultCode  = elements.get(2).decodeAsEnumerated().intValue();
+
+      ASN1OctetString contextID = null;
+      if (elements.size() == 4)
+      {
+        contextID = elements.get(3).decodeAsOctetString();
+      }
+
+      return new VLVResponseControl(control.getOID(), control.isCritical(),
+                                    controlValue, targetPosition, contentCount,
+                                    vlvResultCode, contextID);
+    }
+    catch (LDAPException le)
+    {
+      throw le;
+    }
+    catch (Exception e)
+    {
+      int    msgID   = MSGID_VLVRES_CONTROL_CANNOT_DECODE_VALUE;
+      String message = getMessage(msgID, getExceptionMessage(e));
+      throw new LDAPException(LDAPResultCode.PROTOCOL_ERROR, msgID, message, e);
+    }
+  }
+
+
+
+  /**
+   * Retrieves a string representation of this VLV request control.
+   *
+   * @return  A string representation of this VLV request control.
+   */
+  public String toString()
+  {
+    StringBuilder buffer = new StringBuilder();
+    toString(buffer);
+    return buffer.toString();
+  }
+
+
+
+  /**
+   * Appends a string representation of this VLV request control to the provided
+   * buffer.
+   *
+   * @param  buffer  The buffer to which the information should be appended.
+   */
+  public void toString(StringBuilder buffer)
+  {
+    buffer.append("VLVResponseControl(targetPosition=");
+    buffer.append(targetPosition);
+    buffer.append(", contentCount=");
+    buffer.append(contentCount);
+    buffer.append(", vlvResultCode=");
+    buffer.append(vlvResultCode);
+
+    if (contextID != null)
+    {
+      buffer.append(", contextID=");
+      buffer.append(contextID);
+    }
+
+    buffer.append(")");
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/CoreMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/CoreMessages.java
index e63864a..d27a8e9 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/CoreMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/CoreMessages.java
@@ -6092,6 +6092,36 @@
 
 
   /**
+   * The message ID for the string representation of the result code that will
+   * be used for search operations containing the VLV request control that do
+   * not also include the server-side sort control.
+   */
+  public static final int MSGID_RESULT_SORT_CONTROL_MISSING =
+       CATEGORY_MASK_CORE | SEVERITY_MASK_INFORMATIONAL | 608;
+
+
+
+  /**
+   * The message ID for the string representation of the result code that will
+   * be used for search operations containing the VLV request control with an
+   * invalid offset or target count.
+   */
+  public static final int MSGID_RESULT_OFFSET_RANGE_ERROR =
+       CATEGORY_MASK_CORE | SEVERITY_MASK_INFORMATIONAL | 609;
+
+
+
+  /**
+   * The message ID for the string representation of the result code that will
+   * be used for operations that failed because the request would have impacted
+   * information in multiple servers or repositories.
+   */
+  public static final int MSGID_RESULT_VIRTUAL_LIST_VIEW_ERROR =
+       CATEGORY_MASK_CORE | SEVERITY_MASK_INFORMATIONAL | 610;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined
    * in this class.
    */
@@ -6219,6 +6249,8 @@
     registerMessage(MSGID_RESULT_UNAVAILABLE, "Unavailable");
     registerMessage(MSGID_RESULT_UNWILLING_TO_PERFORM, "Unwilling to Perform");
     registerMessage(MSGID_RESULT_LOOP_DETECT, "Loop Detected");
+    registerMessage(MSGID_RESULT_SORT_CONTROL_MISSING, "Sort Control Missing");
+    registerMessage(MSGID_RESULT_OFFSET_RANGE_ERROR, "Offset Range Error");
     registerMessage(MSGID_RESULT_NAMING_VIOLATION, "Naming Violation");
     registerMessage(MSGID_RESULT_OBJECTCLASS_VIOLATION,
                     "ObjectClass Violation");
@@ -6230,6 +6262,8 @@
                     "ObjectClass Modifications Prohibited");
     registerMessage(MSGID_RESULT_AFFECTS_MULTIPLE_DSAS,
                     "Affects Multiple DSAs");
+    registerMessage(MSGID_RESULT_VIRTUAL_LIST_VIEW_ERROR,
+                    "Virtual List View Error");
     registerMessage(MSGID_RESULT_OTHER, "Other");
     registerMessage(MSGID_RESULT_CLIENT_SIDE_SERVER_DOWN, "Server Down");
     registerMessage(MSGID_RESULT_CLIENT_SIDE_LOCAL_ERROR, "Local Error");
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/JebMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/JebMessages.java
index bff6ec0..4c68e21 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/JebMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/JebMessages.java
@@ -1112,6 +1112,37 @@
        CATEGORY_MASK_JEB | SEVERITY_MASK_MILD_ERROR | 140;
 
   /**
+   * The message ID to use if a VLV request has a negative start position.  This
+   * does not take any arguments.
+   */
+  public static final int MSGID_ENTRYIDSORTER_NEGATIVE_START_POS =
+       CATEGORY_MASK_JEB | SEVERITY_MASK_MILD_ERROR | 141;
+
+  /**
+   * The message ID to use if a VLV request has an offset beyond the end of the
+   * entry set.  This takes two arguments, which are the provided offset and the
+   * list size.
+   */
+  public static final int MSGID_ENTRYIDSORTER_OFFSET_TOO_LARGE =
+       CATEGORY_MASK_JEB | SEVERITY_MASK_MILD_ERROR | 142;
+
+  /**
+   * The message ID to use if a VLV request specifies a target value that is
+   * larger than all values in the sort list.  This does not take any arguments.
+   */
+  public static final int MSGID_ENTRYIDSORTER_TARGET_VALUE_NOT_FOUND =
+       CATEGORY_MASK_JEB | SEVERITY_MASK_MILD_ERROR | 143;
+
+
+  /**
+   * The message ID of an error indicating that the search request included both
+   * the paged results control and the VLV control.  This does not take any
+   * arguments.
+   */
+  public static final int MSGID_JEB_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV =
+       CATEGORY_MASK_JEB | SEVERITY_MASK_MILD_ERROR | 144;
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -1208,6 +1239,11 @@
                     "allow it to be replaced");
     registerMessage(MSGID_JEB_ATTRIBUTE_INDEX_NOT_CONFIGURED,
                     "There is no index configured for attribute type '%s'");
+    registerMessage(MSGID_JEB_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV,
+                    "The requested search operation included both the simple " +
+                    "paged results control and the virtual list view " +
+                    "control.  These controls are mutually exclusive and " +
+                    "cannot be used together");
     registerMessage(MSGID_JEB_SEARCH_NO_SUCH_OBJECT,
                     "The search base entry '%s' does not exist");
     registerMessage(MSGID_JEB_SEARCH_CANNOT_SORT_UNINDEXED,
@@ -1419,5 +1455,17 @@
     registerMessage(MSGID_ENTRYIDSORTER_CANNOT_EXAMINE_ENTRY,
                     "Unable to examine the entry with ID %s for sorting " +
                     "purposes:  %s");
+    registerMessage(MSGID_ENTRYIDSORTER_NEGATIVE_START_POS,
+                    "Unable to process the virtual list view request because " +
+                    "the target start position was before the beginning of " +
+                    "the result set");
+    registerMessage(MSGID_ENTRYIDSORTER_OFFSET_TOO_LARGE,
+                    "Unable to process the virtual list view request because " +
+                    "the target offset %d was greater than the total number " +
+                    "of results in the list (%d)");
+    registerMessage(MSGID_ENTRYIDSORTER_TARGET_VALUE_NOT_FOUND,
+                    "Unable to prcess the virtual list view request because " +
+                    "no entry was found in the result set with a sort value " +
+                    "greater than or equal to the provided assertion value");
   }
 }
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ProtocolMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ProtocolMessages.java
index b54475a..156a19c 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ProtocolMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ProtocolMessages.java
@@ -4510,6 +4510,74 @@
 
 
   /**
+   * The message ID for the message that will be used if the VLV request control
+   * does not have a value.  It does not take any arguments.
+   */
+  public static final int MSGID_VLVREQ_CONTROL_NO_VALUE =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 417;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV request control
+   * has an invalid number of elements.  This takes a single argument, which is
+   * the number of elements contained in the control sequence.
+   */
+  public static final int MSGID_VLVREQ_CONTROL_INVALID_ELEMENT_COUNT =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 418;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV request control
+   * has an invalid BER target type.  It takes a single argument, which is the
+   * hex representation of the BER target type.
+   */
+  public static final int MSGID_VLVREQ_CONTROL_INVALID_TARGET_TYPE =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 419;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV request control
+   * value cannot be decoded.  It takes a single argument, which is a message
+   * explaining the problem that occurred.
+   */
+  public static final int MSGID_VLVREQ_CONTROL_CANNOT_DECODE_VALUE =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 420;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV response
+   * control does not have a value.  It does not take any arguments.
+   */
+  public static final int MSGID_VLVRES_CONTROL_NO_VALUE =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 421;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV response
+   * control has an invalid number of elements.  This takes a single argument,
+   * which is the number of elements contained in the control sequence.
+   */
+  public static final int MSGID_VLVRES_CONTROL_INVALID_ELEMENT_COUNT =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 422;
+
+
+
+  /**
+   * The message ID for the message that will be used if the VLV response
+   * control value cannot be decoded.  It takes a single argument, which is a
+   * message explaining the problem that occurred.
+   */
+  public static final int MSGID_VLVRES_CONTROL_CANNOT_DECODE_VALUE =
+       CATEGORY_MASK_PROTOCOL | SEVERITY_MASK_INFORMATIONAL | 423;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -6444,6 +6512,34 @@
                     "Unable to process the provided server-side sort " +
                     "response control because an error occurred while " +
                     "attempting to decode the control value:  %s");
-  }
+
+
+    registerMessage(MSGID_VLVREQ_CONTROL_NO_VALUE,
+                    "Unable to decode the provided control as a VLV request " +
+                    "control because it does not include a control value");
+    registerMessage(MSGID_VLVREQ_CONTROL_INVALID_ELEMENT_COUNT,
+                    "Unable to decode the provided control as a VLV request " +
+                    "control because it contains an invalid number of " +
+                    "elements:  %d");
+    registerMessage(MSGID_VLVREQ_CONTROL_INVALID_TARGET_TYPE,
+                    "Unable to decode the provided control as a VLV request " +
+                    "control because the target element type %s is invalid");
+    registerMessage(MSGID_VLVREQ_CONTROL_CANNOT_DECODE_VALUE,
+                    "Unable to process the provided VLV request control " +
+                    "because an error occurred while attempting to decode " +
+                    "the control value:  %s");
+
+
+    registerMessage(MSGID_VLVRES_CONTROL_NO_VALUE,
+                    "Unable to decode the provided control as a VLV response " +
+                    "control because it does not include a control value");
+    registerMessage(MSGID_VLVRES_CONTROL_INVALID_ELEMENT_COUNT,
+                    "Unable to decode the provided control as a VLV response " +
+                    "control because it contains an invalid number of " +
+                    "elements:  %d");
+    registerMessage(MSGID_VLVRES_CONTROL_CANNOT_DECODE_VALUE,
+                    "Unable to process the provided VLV response control " +
+                    "because an error occurred while attempting to decode " +
+                    "the control value:  %s");  }
 }
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ToolMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ToolMessages.java
index 880adbe..c7647b3 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ToolMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ToolMessages.java
@@ -6968,6 +6968,98 @@
 
 
   /**
+   * The message ID for the message that will be used as the description of the
+   * virtualListView option for the ldapsearch tool.  It does not take any
+   * arguments.
+   */
+  public static final int MSGID_DESCRIPTION_VLV =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 878;
+
+
+
+  /**
+   * The message ID for the message that will be used if the user requests a VLV
+   * operation without also requesting a server-side sort.  This takes two
+   * arguments, which are the long identifiers for the virtualListView and
+   * sortOrder arguments.
+   */
+  public static final int MSGID_LDAPSEARCH_VLV_REQUIRES_SORT =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_MILD_ERROR | 879;
+
+
+
+  /**
+   * The message ID for the message that will be used if the user requests a VLV
+   * operation with an invalid descriptor.  This does not take any arguments.
+   */
+  public static final int MSGID_LDAPSEARCH_VLV_INVALID_DESCRIPTOR =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_MILD_ERROR | 880;
+
+
+
+  /**
+   * The message ID for the message that will be used to indicate that the
+   * requested server-side sort operation was not successful.  This takes a
+   * single argument, which is a string representation of the associated sort
+   * control result code.
+   */
+  public static final int MSGID_LDAPSEARCH_SORT_ERROR =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_SEVERE_WARNING | 881;
+
+
+
+  /**
+   * The message ID for the message that will be used to indicate that the
+   * server-side sort response control could not be decoded.  This takes a
+   * single argument, which is a message explaining the problem that occurred.
+   */
+  public static final int MSGID_LDAPSEARCH_CANNOT_DECODE_SORT_RESPONSE =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_SEVERE_WARNING | 882;
+
+
+
+  /**
+   * The message ID for the message that will be used display the target offset
+   * for the VLV result set.  This takes a single argument, which is the target
+   * offset.
+   */
+  public static final int MSGID_LDAPSEARCH_VLV_TARGET_OFFSET =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 883;
+
+
+
+  /**
+   * The message ID for the message that will be used display the content count
+   * for the VLV result set.  This takes a single argument, which is the content
+   * count.
+   */
+  public static final int MSGID_LDAPSEARCH_VLV_CONTENT_COUNT =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 884;
+
+
+
+  /**
+   * The message ID for the message that will be used to indicate that the
+   * requested virtual list view operation was not successful.  This takes a
+   * single argument, which is a string representation of the associated sort
+   * control result code.
+   */
+  public static final int MSGID_LDAPSEARCH_VLV_ERROR =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_SEVERE_WARNING | 885;
+
+
+
+  /**
+   * The message ID for the message that will be used to indicate that the
+   * virtual list view response control could not be decoded.  This takes a
+   * single argument, which is a message explaining the problem that occurred.
+   */
+  public static final int MSGID_LDAPSEARCH_CANNOT_DECODE_VLV_RESPONSE =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_SEVERE_WARNING | 886;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -7389,6 +7481,9 @@
                     "filter");
     registerMessage(MSGID_DESCRIPTION_SORT_ORDER,
                     "Sort the results using the provided sort order");
+    registerMessage(MSGID_DESCRIPTION_VLV,
+                    "Use the virtual list view control to retrieve the " +
+                    "specified results page");
     registerMessage(MSGID_COMPARE_CANNOT_BASE64_DECODE_ASSERTION_VALUE,
                     "The assertion value was indicated to be base64-encoded, " +
                     "but an error occurred while trying to decode the value");
@@ -7682,6 +7777,23 @@
                     "The provided matched values filter was invalid:  %s");
     registerMessage(MSGID_LDAP_SORTCONTROL_INVALID_ORDER,
                     "The provided sort order was invalid:  %s");
+    registerMessage(MSGID_LDAPSEARCH_VLV_REQUIRES_SORT,
+                    "If the --%s argument is provided, then the --%s " +
+                    "argument must also be given");
+    registerMessage(MSGID_LDAPSEARCH_VLV_INVALID_DESCRIPTOR,
+                    "The provided virtual list view descriptor was invalid.  " +
+                    "It must be a value in the form " +
+                    "'beforeCount:afterCount:offset:contentCount' (where " +
+                    "offset specifies the index of the target entry and " +
+                    "contentCount specifies the estimated total number of " +
+                    "results or zero if it is not known), or " +
+                    "'beforeCount:afterCount:assertionValue' (where the " +
+                    "entry should be the first entry whose primary sort " +
+                    "value is greater than or equal to the provided " +
+                    "assertionValue).  In either case, beforeCount is the " +
+                    "number of entries to return before the target value and " +
+                    "afterCount is the number of entries to return after " +
+                    "the target value");
     registerMessage(MSGID_LDAPMODIFY_PREREAD_NO_VALUE,
                     "The pre-read response control did not include a value");
     registerMessage(MSGID_LDAPMODIFY_PREREAD_CANNOT_DECODE_VALUE,
@@ -8886,6 +8998,18 @@
                     "#   The account is locked");
     registerMessage(MSGID_LDAPSEARCH_ACCTUSABLE_TIME_UNTIL_UNLOCK,
                     "#   Time until the account is unlocked:  %s");
+    registerMessage(MSGID_LDAPSEARCH_SORT_ERROR,
+                    "# Server-side sort failed:  %s");
+    registerMessage(MSGID_LDAPSEARCH_CANNOT_DECODE_SORT_RESPONSE,
+                    "# Unable to decode the server-side sort response:  %s");
+    registerMessage(MSGID_LDAPSEARCH_VLV_TARGET_OFFSET,
+                    "# VLV Target Offset:  %d");
+    registerMessage(MSGID_LDAPSEARCH_VLV_CONTENT_COUNT,
+                    "# VLV Content Count:  %d");
+    registerMessage(MSGID_LDAPSEARCH_VLV_ERROR,
+                    "# Virtual list view processing failed:  %s");
+    registerMessage(MSGID_LDAPSEARCH_CANNOT_DECODE_VLV_RESPONSE,
+                    "# Unable to decode the virtual list view response:  %s");
     registerMessage(MSGID_LDAPSEARCH_MATCHING_ENTRY_COUNT,
                     "# Total number of matching entries:  %d");
 
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/ldap/LDAPResultCode.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/ldap/LDAPResultCode.java
index 9fc0e0f..114cb18 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/ldap/LDAPResultCode.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/ldap/LDAPResultCode.java
@@ -287,6 +287,22 @@
 
 
   /**
+   * The LDAP result code for operations that fail because the request included
+   * a VLV request control without a server-side sort control.
+   */
+  public static final int SORT_CONTROL_MISSING = 60;
+
+
+
+  /**
+   * The LDAP result code for operations that fail because the request included
+   * a VLV request control with an invalid offset.
+   */
+  public static final int OFFSET_RANGE_ERROR = 61;
+
+
+
+  /**
    * The LDAP result code for operations that fail due to a naming violation.
    */
   public static final int NAMING_VIOLATION = 64;
@@ -343,6 +359,14 @@
 
 
   /**
+   * The LDAP result code for operations that fail due to an error in
+   * virtual list view processing.
+   */
+  public static final int VIRTUAL_LIST_VIEW_ERROR = 76;
+
+
+
+  /**
    * The LDAP result code for use in cases in which none of the other defined
    * result codes are appropriate.
    */
@@ -664,6 +688,12 @@
       case LOOP_DETECT:
         msgID = MSGID_RESULT_LOOP_DETECT;
         break;
+      case SORT_CONTROL_MISSING:
+        msgID = MSGID_RESULT_SORT_CONTROL_MISSING;
+        break;
+      case OFFSET_RANGE_ERROR:
+        msgID = MSGID_RESULT_OFFSET_RANGE_ERROR;
+        break;
       case NAMING_VIOLATION:
         msgID = MSGID_RESULT_NAMING_VIOLATION;
         break;
@@ -685,6 +715,9 @@
       case AFFECTS_MULTIPLE_DSAS:
         msgID = MSGID_RESULT_AFFECTS_MULTIPLE_DSAS;
         break;
+      case VIRTUAL_LIST_VIEW_ERROR:
+        msgID = MSGID_RESULT_VIRTUAL_LIST_VIEW_ERROR;
+        break;
       case CLIENT_SIDE_SERVER_DOWN:
         msgID = MSGID_RESULT_CLIENT_SIDE_SERVER_DOWN;
         break;
diff --git a/opendj-sdk/opends/src/server/org/opends/server/tools/LDAPSearch.java b/opendj-sdk/opends/src/server/org/opends/server/tools/LDAPSearch.java
index adbb71c..b799511 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/tools/LDAPSearch.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/tools/LDAPSearch.java
@@ -46,6 +46,9 @@
 import org.opends.server.controls.PersistentSearchChangeType;
 import org.opends.server.controls.PersistentSearchControl;
 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.util.Base64;
 import org.opends.server.util.PasswordReader;
@@ -63,6 +66,7 @@
 import org.opends.server.protocols.ldap.LDAPControl;
 import org.opends.server.protocols.ldap.LDAPFilter;
 import org.opends.server.protocols.ldap.LDAPMessage;
+import org.opends.server.protocols.ldap.LDAPResultCode;
 import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
 import org.opends.server.protocols.ldap.SearchResultDoneProtocolOp;
 import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp;
@@ -301,6 +305,66 @@
                 errorMessage = searchOp.getErrorMessage();
                 matchedDN = searchOp.getMatchedDN();
 
+                for (LDAPControl c : responseMessage.getControls())
+                {
+                  if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+                  {
+                    try
+                    {
+                      ServerSideSortResponseControl sortResponse =
+                           ServerSideSortResponseControl.decodeControl(
+                                c.getControl());
+                      int rc = sortResponse.getResultCode();
+                      if (rc != LDAPResultCode.SUCCESS)
+                      {
+                        int    msgID = MSGID_LDAPSEARCH_SORT_ERROR;
+                        String msg   = getMessage(msgID,
+                                                  LDAPResultCode.toString(rc));
+                        err.println(msg);
+                      }
+                    }
+                    catch (Exception e)
+                    {
+                      int msgID = MSGID_LDAPSEARCH_CANNOT_DECODE_SORT_RESPONSE;
+                      String msg   = getMessage(msgID, getExceptionMessage(e));
+                      err.println(msg);
+                    }
+                  }
+                  else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+                  {
+                    try
+                    {
+                      VLVResponseControl vlvResponse =
+                           VLVResponseControl.decodeControl(c.getControl());
+                      int rc = vlvResponse.getVLVResultCode();
+                      if (rc == LDAPResultCode.SUCCESS)
+                      {
+                        int msgID = MSGID_LDAPSEARCH_VLV_TARGET_OFFSET;
+                        String msg = getMessage(msgID,
+                                          vlvResponse.getTargetPosition());
+                        out.println(msg);
+
+                        msgID = MSGID_LDAPSEARCH_VLV_CONTENT_COUNT;
+                        msg = getMessage(msgID, vlvResponse.getContentCount());
+                        out.println(msg);
+                      }
+                      else
+                      {
+                        int msgID = MSGID_LDAPSEARCH_VLV_ERROR;
+                        String msg = getMessage(msgID,
+                                                LDAPResultCode.toString(rc));
+                        err.println(msg);
+                      }
+                    }
+                    catch (Exception e)
+                    {
+                      int msgID = MSGID_LDAPSEARCH_CANNOT_DECODE_VLV_RESPONSE;
+                      String msg   = getMessage(msgID, getExceptionMessage(e));
+                      err.println(msg);
+                    }
+                  }
+                }
+
                 break;
               default:
                 // FIXME - throw exception?
@@ -606,6 +670,7 @@
     StringArgument    sortOrder                = null;
     StringArgument    trustStorePath           = null;
     StringArgument    trustStorePassword       = null;
+    StringArgument    vlvDescriptor            = null;
 
 
     // Create the command-line argument parser for use with this program.
@@ -795,6 +860,13 @@
                                      MSGID_DESCRIPTION_SORT_ORDER);
       argParser.addArgument(sortOrder);
 
+      vlvDescriptor =
+           new StringArgument("vlvdescriptor", 'G', "virtualListView", false,
+                              false, true,
+                              "{before:after:index:count | before:after:value}",
+                              null, null, MSGID_DESCRIPTION_VLV);
+      argParser.addArgument(vlvDescriptor);
+
       controlStr =
            new StringArgument("control", 'J', "control", false, true, true,
                     "{controloid[:criticality[:value|::b64value|:<fileurl]]}",
@@ -1284,6 +1356,69 @@
       }
     }
 
+    if (vlvDescriptor.isPresent())
+    {
+      if (! sortOrder.isPresent())
+      {
+        int    msgID   = MSGID_LDAPSEARCH_VLV_REQUIRES_SORT;
+        String message = getMessage(msgID, vlvDescriptor.getLongIdentifier(),
+                                    sortOrder.getLongIdentifier());
+        err.println(wrapText(message, MAX_LINE_WIDTH));
+        return 1;
+      }
+
+      StringTokenizer tokenizer =
+           new StringTokenizer(vlvDescriptor.getValue(), ":");
+      int numTokens = tokenizer.countTokens();
+      if (numTokens == 3)
+      {
+        try
+        {
+          int beforeCount = Integer.parseInt(tokenizer.nextToken());
+          int afterCount  = Integer.parseInt(tokenizer.nextToken());
+          ASN1OctetString assertionValue =
+               new ASN1OctetString(tokenizer.nextToken());
+          searchOptions.getControls().add(
+               new LDAPControl(new VLVRequestControl(beforeCount, afterCount,
+                                                     assertionValue)));
+        }
+        catch (Exception e)
+        {
+          int    msgID   = MSGID_LDAPSEARCH_VLV_INVALID_DESCRIPTOR;
+          String message = getMessage(msgID);
+          err.println(wrapText(message, MAX_LINE_WIDTH));
+          return 1;
+        }
+      }
+      else if (numTokens == 4)
+      {
+        try
+        {
+          int beforeCount  = Integer.parseInt(tokenizer.nextToken());
+          int afterCount   = Integer.parseInt(tokenizer.nextToken());
+          int offset       = Integer.parseInt(tokenizer.nextToken());
+          int contentCount = Integer.parseInt(tokenizer.nextToken());
+          searchOptions.getControls().add(
+               new LDAPControl(new VLVRequestControl(beforeCount, afterCount,
+                                                     offset, contentCount)));
+        }
+        catch (Exception e)
+        {
+          int    msgID   = MSGID_LDAPSEARCH_VLV_INVALID_DESCRIPTOR;
+          String message = getMessage(msgID);
+          err.println(wrapText(message, MAX_LINE_WIDTH));
+          return 1;
+        }
+      }
+      else
+      {
+        int    msgID   = MSGID_LDAPSEARCH_VLV_INVALID_DESCRIPTOR;
+        String message = getMessage(msgID);
+        err.println(wrapText(message, MAX_LINE_WIDTH));
+        return 1;
+      }
+    }
+
     // Set the connection options.
     connectionOptions.setSASLExternal(saslExternal.isPresent());
     if(saslOptions.isPresent())
diff --git a/opendj-sdk/opends/src/server/org/opends/server/types/ResultCode.java b/opendj-sdk/opends/src/server/org/opends/server/types/ResultCode.java
index 0c3ea76..e5b60c6 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/types/ResultCode.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/types/ResultCode.java
@@ -341,6 +341,24 @@
 
 
   /**
+   * The result code that indicates that a search request included a
+   * VLV request control without a server-side sort control.
+   */
+  SORT_CONTROL_MISSING(LDAPResultCode.SORT_CONTROL_MISSING,
+                       MSGID_RESULT_SORT_CONTROL_MISSING),
+
+
+
+  /**
+   * The result code that indicates that a search request included a
+   * VLV request control with an invalid offset.
+   */
+  OFFSET_RANGE_ERROR(LDAPResultCode.OFFSET_RANGE_ERROR,
+                     MSGID_RESULT_OFFSET_RANGE_ERROR),
+
+
+
+  /**
    * The result code that indicates that the requested operation
    * failed because it would have violated the server's naming
    * configuration.
@@ -410,6 +428,16 @@
 
 
   /**
+   * The result code that indicates that the operation could not be
+   * processed because there was an error while processing the virtual
+   * list view control.
+   */
+  VIRTUAL_LIST_VIEW_ERROR(LDAPResultCode.VIRTUAL_LIST_VIEW_ERROR,
+                          MSGID_RESULT_VIRTUAL_LIST_VIEW_ERROR),
+
+
+
+  /**
    * The result code that should be used if no other result code is
    * appropriate.
    */
@@ -761,6 +789,10 @@
         return UNWILLING_TO_PERFORM;
       case LDAPResultCode.LOOP_DETECT:
         return LOOP_DETECT;
+      case LDAPResultCode.SORT_CONTROL_MISSING:
+        return SORT_CONTROL_MISSING;
+      case LDAPResultCode.OFFSET_RANGE_ERROR:
+        return OFFSET_RANGE_ERROR;
       case LDAPResultCode.NAMING_VIOLATION:
         return NAMING_VIOLATION;
       case LDAPResultCode.OBJECTCLASS_VIOLATION:
@@ -775,6 +807,8 @@
         return OBJECTCLASS_MODS_PROHIBITED;
       case LDAPResultCode.AFFECTS_MULTIPLE_DSAS:
         return AFFECTS_MULTIPLE_DSAS;
+      case LDAPResultCode.VIRTUAL_LIST_VIEW_ERROR:
+        return VIRTUAL_LIST_VIEW_ERROR;
       case LDAPResultCode.CLIENT_SIDE_SERVER_DOWN:
         return CLIENT_SIDE_SERVER_DOWN;
       case LDAPResultCode.CLIENT_SIDE_LOCAL_ERROR:
diff --git a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
index 8f49118..79cc242 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/util/ServerConstants.java
@@ -1851,6 +1851,22 @@
 
 
 
+  /**
+   * The OID for the virtual list view request control.
+   */
+  public static final String OID_VLV_REQUEST_CONTROL =
+       "2.16.840.1.113730.3.4.9";
+
+
+
+  /**
+   * The OID for the virtual list view request control.
+   */
+  public static final String OID_VLV_RESPONSE_CONTROL =
+       "2.16.840.1.113730.3.4.10";
+
+
+
 
   /**
    * The block length in bytes used when generating an HMAC-MD5 digest.
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/VLVControlTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/VLVControlTestCase.java
new file mode 100644
index 0000000..9b63163
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/VLVControlTestCase.java
@@ -0,0 +1,1164 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.controls;
+
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.protocols.ldap.LDAPResultCode;
+import org.opends.server.types.AttributeType;
+import org.opends.server.types.Control;
+import org.opends.server.types.DereferencePolicy;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ResultCode;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchScope;
+import org.opends.server.types.SortKey;
+import org.opends.server.types.SortOrder;
+
+import static org.testng.Assert.*;
+
+import static org.opends.server.util.ServerConstants.*;
+
+
+
+/**
+ * This class contains a number of test cases for the virtual list view request
+ * and response controls.
+ */
+public class VLVControlTestCase
+    extends ControlsTestCase
+{
+  // The givenName attribute type.
+  private AttributeType givenNameType;
+
+  // The sn attribute type.
+  private AttributeType snType;
+
+  // The DN for "Aaccf Johnson"
+  DN aaccfJohnsonDN;
+
+  // The DN for "Aaron Zimmerman"
+  DN aaronZimmermanDN;
+
+  // The DN for "Albert Smith"
+  DN albertSmithDN;
+
+  // The DN for "Albert Zimmerman"
+  DN albertZimmermanDN;
+
+  // The DN for "lowercase mcgee"
+  DN lowercaseMcGeeDN;
+
+  // The DN for "Mararet Jones"
+  DN margaretJonesDN;
+
+  // The DN for "Mary Jones"
+  DN maryJonesDN;
+
+  // The DN for "Sam Zweck"
+  DN samZweckDN;
+
+  // The DN for "Zorro"
+  DN zorroDN;
+
+
+
+  /**
+   * Make sure that the server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+
+    givenNameType = DirectoryServer.getAttributeType("givenname", false);
+    assertNotNull(givenNameType);
+
+    snType = DirectoryServer.getAttributeType("sn", false);
+    assertNotNull(snType);
+
+    aaccfJohnsonDN    = DN.decode("uid=aaccf.johnson,dc=example,dc=com");
+    aaronZimmermanDN  = DN.decode("uid=aaron.zimmerman,dc=example,dc=com");
+    albertSmithDN     = DN.decode("uid=albert.smith,dc=example,dc=com");
+    albertZimmermanDN = DN.decode("uid=albert.zimmerman,dc=example,dc=com");
+    lowercaseMcGeeDN  = DN.decode("uid=lowercase.mcgee,dc=example,dc=com");
+    margaretJonesDN   = DN.decode("uid=margaret.jones,dc=example,dc=com");
+    maryJonesDN       = DN.decode("uid=mary.jones,dc=example,dc=com");
+    samZweckDN        = DN.decode("uid=sam.zweck,dc=example,dc=com");
+    zorroDN           = DN.decode("uid=zorro,dc=example,dc=com");
+  }
+
+
+
+  /**
+   * Populates the JE DB with a set of test data.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  private void populateDB()
+          throws Exception
+  {
+    TestCaseUtils.clearJEBackend(true, "userRoot", "dc=example,dc=com");
+
+    TestCaseUtils.addEntries(
+      "dn: uid=albert.zimmerman,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.zimmerman",
+      "givenName: Albert",
+      "sn: Zimmerman",
+      "cn: Albert Zimmerman",
+      "",
+      "dn: uid=albert.smith,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.smith",
+      "givenName: Albert",
+      "sn: Smith",
+      "cn: Albert Smith",
+      "",
+      "dn: uid=aaron.zimmerman,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.zimmerman",
+      "givenName: Aaron",
+      "givenName: Zeke",
+      "sn: Zimmerman",
+      "cn: Aaron Zimmerman",
+      "",
+      "dn: uid=mary.jones,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: mary.jones",
+      "givenName: Mary",
+      "sn: Jones",
+      "cn: Mary Jones",
+      "",
+      "dn: uid=margaret.jones,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: margaret.jones",
+      "givenName: Margaret",
+      "givenName: Maggie",
+      "sn: Jones",
+      "sn: Smith",
+      "cn: Maggie Jones-Smith",
+      "",
+      "dn: uid=aaccf.johnson,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: aaccf.johnson",
+      "givenName: Aaccf",
+      "sn: Johnson",
+      "cn: Aaccf Johnson",
+      "",
+      "dn: uid=sam.zweck,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: sam.zweck",
+      "givenName: Sam",
+      "sn: Zweck",
+      "cn: Sam Zweck",
+      "",
+      "dn: uid=lowercase.mcgee,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: lowercase.mcgee",
+      "givenName: lowercase",
+      "sn: mcgee",
+      "cn: lowercase mcgee",
+      "",
+      "dn: uid=zorro,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: zorro",
+      "sn: Zorro",
+      "cn: Zorro"
+    );
+  }
+
+
+
+  /**
+   * Tests the first constructor for the request control.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor1()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest = new VLVRequestControl(0, 9, 1, 0);
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getOffset(), 1);
+    assertEquals(vlvRequest.getContentCount(), 0);
+    assertNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
+    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the second constructor for the request control with a null context
+   * ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor2NullContextID()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest = new VLVRequestControl(0, 9, 1, 0, null);
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getOffset(), 1);
+    assertEquals(vlvRequest.getContentCount(), 0);
+    assertNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
+    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the second constructor for the request control with a non-null
+   * context  ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor2NonNullContextID()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest =
+         new VLVRequestControl(0, 9, 1, 0, new ASN1OctetString("foo"));
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getOffset(), 1);
+    assertEquals(vlvRequest.getContentCount(), 0);
+    assertNotNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_BYOFFSET);
+    assertNull(vlvRequest.getGreaterThanOrEqualAssertion());
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the third constructor for the request control.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor3()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest =
+         new VLVRequestControl(0, 9, new ASN1OctetString("a"));
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().stringValue(),
+                 "a");
+    assertNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the fourth constructor for the request control with a null context
+   * ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor4NullContextID()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest =
+         new VLVRequestControl(0, 9, new ASN1OctetString("a"), null);
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().stringValue(),
+                 "a");
+    assertNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the fourth constructor for the request control with a non-null
+   * context ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testRequestConstructor4NonNullContextID()
+              throws Exception
+  {
+    VLVRequestControl vlvRequest =
+         new VLVRequestControl(0, 9, new ASN1OctetString("a"),
+                               new ASN1OctetString("foo"));
+
+    assertEquals(vlvRequest.getBeforeCount(), 0);
+    assertEquals(vlvRequest.getAfterCount(), 9);
+    assertEquals(vlvRequest.getGreaterThanOrEqualAssertion().stringValue(),
+                 "a");
+    assertNotNull(vlvRequest.getContextID());
+    assertEquals(vlvRequest.getTargetType(),
+                 VLVRequestControl.TYPE_TARGET_GREATERTHANOREQUAL);
+    assertNotNull(vlvRequest.toString());
+
+    assertNotNull(vlvRequest.decodeControl(vlvRequest));
+  }
+
+
+
+  /**
+   * Tests the first constructor for the response control.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testResponseConstructor1()
+         throws Exception
+  {
+    VLVResponseControl vlvResponse = new VLVResponseControl(0, 15, 0);
+
+    assertEquals(vlvResponse.getTargetPosition(), 0);
+    assertEquals(vlvResponse.getContentCount(), 15);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertNull(vlvResponse.getContextID());
+    assertNotNull(vlvResponse.toString());
+
+    assertNotNull(vlvResponse.decodeControl(vlvResponse));
+  }
+
+
+
+  /**
+   * Tests the second constructor for the response control with a null context
+   * ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testResponseConstructor2NullContextID()
+         throws Exception
+  {
+    VLVResponseControl vlvResponse = new VLVResponseControl(0, 15, 0, null);
+
+    assertEquals(vlvResponse.getTargetPosition(), 0);
+    assertEquals(vlvResponse.getContentCount(), 15);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertNull(vlvResponse.getContextID());
+    assertNotNull(vlvResponse.toString());
+
+    assertNotNull(vlvResponse.decodeControl(vlvResponse));
+  }
+
+
+
+  /**
+   * Tests the second constructor for the response control with a non-null
+   * context ID.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testResponseConstructor2NonNullContextID()
+         throws Exception
+  {
+    VLVResponseControl vlvResponse =
+         new VLVResponseControl(0, 15, 0, new ASN1OctetString("foo"));
+
+    assertEquals(vlvResponse.getTargetPosition(), 0);
+    assertEquals(vlvResponse.getContentCount(), 15);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertNotNull(vlvResponse.getContextID());
+    assertNotNull(vlvResponse.toString());
+
+    assertNotNull(vlvResponse.decodeControl(vlvResponse));
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an offset of zero.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByOffsetZeroOffset()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3, 1, 0));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
+    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 1);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using a nonzero offset that still is completely
+   * within the bounds of the result set.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByOffsetNonZeroOffset()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3, 3, 0));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
+    expectedDNOrder.add(margaretJonesDN);   // Maggie
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 3);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control with a negative
+   * start position.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByOffsetNegativeStartPosition()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(3, 3, 1, 0));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+
+    // It will be successful because it's not a critical control.
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+
+    VLVResponseControl vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+    }
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(),
+                 LDAPResultCode.OFFSET_RANGE_ERROR);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control with a start
+   * start position beyond the end of the result set.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByOffsetStartPositionTooHigh()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(3, 3, 30, 0));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+
+    // It will be successful because it's not a critical control.
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+
+    VLVResponseControl vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+    }
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(),
+                 LDAPResultCode.OFFSET_RANGE_ERROR);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control with a start
+   * start position within the bounds of the list but not enough remaining
+   * entries to meet the afterCount
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByOffsetIncompleteAfterCount()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3, 7, 0));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(maryJonesDN);       // Mary
+    expectedDNOrder.add(samZweckDN);        // Sam
+    expectedDNOrder.add(zorroDN);           // No first name
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 7);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an assertion value before any actual value in
+   * the list.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByValueBeforeAll()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3, new ASN1OctetString("a")));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
+    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 1);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an assertion value that matches the first value
+   * in the list.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByValueMatchesFirst()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3,
+                                              new ASN1OctetString("aaccf")));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(aaccfJohnsonDN);    // Aaccf
+    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 1);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an assertion value that matches the third value
+   * in the list.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByValueMatchesThird()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(0, 3,
+                                              new ASN1OctetString("albert")));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
+    expectedDNOrder.add(margaretJonesDN);   // Maggie
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 3);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an assertion value that matches the third value
+   * in the list and includes a nonzero before count.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByValueMatchesThirdWithBeforeCount()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("givenName"));
+    requestControls.add(new VLVRequestControl(1, 3,
+                                              new ASN1OctetString("albert")));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    ArrayList<DN> expectedDNOrder = new ArrayList<DN>();
+    expectedDNOrder.add(aaronZimmermanDN);  // Aaron
+    expectedDNOrder.add(albertZimmermanDN); // Albert, lower entry ID
+    expectedDNOrder.add(albertSmithDN);     // Albert, higher entry ID
+    expectedDNOrder.add(lowercaseMcGeeDN);  // lowercase
+    expectedDNOrder.add(margaretJonesDN);   // Maggie
+
+    ArrayList<DN> returnedDNOrder = new ArrayList<DN>();
+    for (Entry e : internalSearch.getSearchEntries())
+    {
+      returnedDNOrder.add(e.getDN());
+    }
+
+    assertEquals(returnedDNOrder, expectedDNOrder);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+    assertEquals(responseControls.size(), 2);
+
+    ServerSideSortResponseControl sortResponse = null;
+    VLVResponseControl            vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_SERVER_SIDE_SORT_RESPONSE_CONTROL))
+      {
+        sortResponse = ServerSideSortResponseControl.decodeControl(c);
+      }
+      else if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+      else
+      {
+        fail("Response control with unexpected OID " + c.getOID());
+      }
+    }
+
+    assertNotNull(sortResponse);
+    assertEquals(sortResponse.getResultCode(), 0);
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(), 0);
+    assertEquals(vlvResponse.getTargetPosition(), 3);
+    assertEquals(vlvResponse.getContentCount(), 9);
+  }
+
+
+
+  /**
+   * Tests performing an internal search using the VLV control to retrieve a
+   * subset of the entries using an assertion value that is after all values in
+   * the list.
+   *
+   * @throws  Exception  If an unexpected problem occurred.
+   */
+  @Test()
+  public void testInternalSearchByValueAfterAll()
+         throws Exception
+  {
+    populateDB();
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+
+    ArrayList<Control> requestControls = new ArrayList<Control>();
+    requestControls.add(new ServerSideSortRequestControl("sn"));
+    requestControls.add(new VLVRequestControl(0, 3, new ASN1OctetString("zz")));
+
+    InternalSearchOperation internalSearch =
+         new InternalSearchOperation(conn, conn.nextOperationID(),
+                  conn.nextMessageID(), requestControls,
+                  DN.decode("dc=example,dc=com"), SearchScope.WHOLE_SUBTREE,
+                  DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false,
+                  SearchFilter.createFilterFromString("(objectClass=person)"),
+                  null, null);
+    internalSearch.run();
+
+    // It will be successful because the control isn't critical.
+    assertEquals(internalSearch.getResultCode(), ResultCode.SUCCESS);
+
+    List<Control> responseControls = internalSearch.getResponseControls();
+    assertNotNull(responseControls);
+
+    VLVResponseControl vlvResponse  = null;
+    for (Control c : responseControls)
+    {
+      if (c.getOID().equals(OID_VLV_RESPONSE_CONTROL))
+      {
+        vlvResponse = VLVResponseControl.decodeControl(c);
+      }
+    }
+
+    assertNotNull(vlvResponse);
+    assertEquals(vlvResponse.getVLVResultCode(),
+                 LDAPResultCode.OFFSET_RANGE_ERROR);
+  }
+}
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/LDAPSearchTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/LDAPSearchTestCase.java
index f225b16..b641d03 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/LDAPSearchTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/LDAPSearchTestCase.java
@@ -2273,6 +2273,271 @@
 
 
   /**
+   * Tests the use of the virtual list view control without the server-side sort
+   * control.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVLVWithoutSort()
+         throws Exception
+  {
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-G", "0:9:1:0",
+      "(objectClass=*)"
+    };
+
+    assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+  }
+
+
+
+  /**
+   * Tests the use of the server-side sort control with both the simple paged
+   * results and virtual list view controls.
+   *
+   * @throws  Exception  If an unexpectd problem occurs.
+   */
+  @Test()
+  public void testSortWithVLVAndPagedResults()
+         throws Exception
+  {
+    TestCaseUtils.clearJEBackend(true, "userRoot", "dc=example,dc=com");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-S", "sn,givenName",
+      "--simplePageSize", "2",
+      "-G", "0:3:1:0",
+      "(objectClass=*)"
+    };
+
+    assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+  }
+
+
+
+  /**
+   * Tests the use of the virtual list view control with an invalid descriptor
+   * with no colons.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVLVInvalidDescriptorNoColons()
+         throws Exception
+  {
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-S", "sn,givenName",
+      "-G", "invalid",
+      "(objectClass=*)"
+    };
+
+    assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+  }
+
+
+
+  /**
+   * Tests the use of the virtual list view control with an invalid descriptor
+   * with two colons.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVLVInvalidDescriptorTwoColons()
+         throws Exception
+  {
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-S", "sn,givenName",
+      "-G", "invalid:9:invalid",
+      "(objectClass=*)"
+    };
+
+    assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+  }
+
+
+
+  /**
+   * Tests the use of the virtual list view control with an invalid descriptor
+   * with three colons.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testVLVInvalidDescriptorThreeColons()
+         throws Exception
+  {
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-S", "sn,givenName",
+      "-G", "invalid:9:1:0",
+      "(objectClass=*)"
+    };
+
+    assertFalse(LDAPSearch.mainSearch(args, false, null, null) == 0);
+  }
+
+
+
+  /**
+   * Tests the use of both the server-side sort control and the virtual list
+   * view control.
+   *
+   * @throws  Exception  If an unexpectd problem occurs.
+   */
+  @Test()
+  public void testSortWithVLV()
+         throws Exception
+  {
+    TestCaseUtils.clearJEBackend(true, "userRoot", "dc=example,dc=com");
+
+    TestCaseUtils.addEntries(
+      "dn: uid=albert.zimmerman,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.zimmerman",
+      "givenName: Albert",
+      "sn: Zimmerman",
+      "cn: Albert Zimmerman",
+      "",
+      "dn: uid=albert.smith,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.smith",
+      "givenName: Albert",
+      "sn: Smith",
+      "cn: Albert Smith",
+      "",
+      "dn: uid=aaron.zimmerman,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: albert.zimmerman",
+      "givenName: Aaron",
+      "givenName: Zeke",
+      "sn: Zimmerman",
+      "cn: Aaron Zimmerman",
+      "",
+      "dn: uid=mary.jones,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: mary.jones",
+      "givenName: Mary",
+      "sn: Jones",
+      "cn: Mary Jones",
+      "",
+      "dn: uid=margaret.jones,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: margaret.jones",
+      "givenName: Margaret",
+      "givenName: Maggie",
+      "sn: Jones",
+      "sn: Smith",
+      "cn: Maggie Jones-Smith",
+      "",
+      "dn: uid=aaccf.johnson,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: aaccf.johnson",
+      "givenName: Aaccf",
+      "sn: Johnson",
+      "cn: Aaccf Johnson",
+      "",
+      "dn: uid=sam.zweck,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: sam.zweck",
+      "givenName: Sam",
+      "sn: Zweck",
+      "cn: Sam Zweck",
+      "",
+      "dn: uid=lowercase.mcgee,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: lowercase.mcgee",
+      "givenName: lowercase",
+      "sn: mcgee",
+      "cn: lowercase mcgee",
+      "",
+      "dn: uid=zorro,dc=example,dc=com",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: zorro",
+      "sn: Zorro",
+      "cn: Zorro");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "-b", "dc=example,dc=com",
+      "-s", "sub",
+      "-S", "givenName",
+      "-G", "1:3:1:0",
+      "(objectClass=*)"
+    };
+
+    assertEquals(LDAPSearch.mainSearch(args, false, null, System.err), 0);
+  }
+
+
+
+  /**
    * Tests the LDAPSearch tool with the "--help" option.
    */
   @Test()

--
Gitblit v1.10.0