You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by ka...@apache.org on 2013/12/27 08:30:55 UTC

svn commit: r1553637 - in /directory/apacheds/trunk: core-shared/ core-shared/src/main/java/org/apache/directory/server/core/shared/ protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/ protocol-ldap/src/main/java/org/apache/d...

Author: kayyagari
Date: Fri Dec 27 07:30:55 2013
New Revision: 1553637

URL: http://svn.apache.org/r1553637
Log:
added support for sorting entries based on the sort  request control (DIRSERVER-264)

Added:
    directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryComparator.java
    directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryCursor.java
    directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntrySerializer.java
    directory/apacheds/trunk/server-integ/src/test/java/org/apache/directory/server/operations/search/SortedSearchIT.java
    directory/apacheds/trunk/server-integ/src/test/resources/sortedsearch-test-data.ldif
Modified:
    directory/apacheds/trunk/core-shared/pom.xml
    directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/DefaultCoreSession.java
    directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/SearchRequestHandler.java
    directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/replication/provider/SyncReplRequestHandler.java

Modified: directory/apacheds/trunk/core-shared/pom.xml
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core-shared/pom.xml?rev=1553637&r1=1553636&r2=1553637&view=diff
==============================================================================
--- directory/apacheds/trunk/core-shared/pom.xml (original)
+++ directory/apacheds/trunk/core-shared/pom.xml Fri Dec 27 07:30:55 2013
@@ -80,8 +80,12 @@
     <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache-core</artifactId>
-   </dependency>
+    </dependency>
 
+    <dependency>
+       <groupId>org.apache.directory.mavibot</groupId>
+       <artifactId>mavibot</artifactId>
+    </dependency>
   </dependencies>
 
   <build>

Modified: directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/DefaultCoreSession.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/DefaultCoreSession.java?rev=1553637&r1=1553636&r2=1553637&view=diff
==============================================================================
--- directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/DefaultCoreSession.java (original)
+++ directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/DefaultCoreSession.java Fri Dec 27 07:30:55 2013
@@ -20,17 +20,22 @@
 package org.apache.directory.server.core.shared;
 
 
+import java.io.File;
+import java.io.IOException;
 import java.net.SocketAddress;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 
 import org.apache.directory.api.ldap.extras.controls.SyncRequestValue;
 import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
 import org.apache.directory.api.ldap.model.constants.SchemaConstants;
 import org.apache.directory.api.ldap.model.cursor.Cursor;
+import org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.EmptyCursor;
 import org.apache.directory.api.ldap.model.entry.BinaryValue;
 import org.apache.directory.api.ldap.model.entry.DefaultModification;
 import org.apache.directory.api.ldap.model.entry.Entry;
@@ -47,15 +52,31 @@ import org.apache.directory.api.ldap.mod
 import org.apache.directory.api.ldap.model.message.CompareRequest;
 import org.apache.directory.api.ldap.model.message.Control;
 import org.apache.directory.api.ldap.model.message.DeleteRequest;
+import org.apache.directory.api.ldap.model.message.LdapResult;
 import org.apache.directory.api.ldap.model.message.ModifyDnRequest;
 import org.apache.directory.api.ldap.model.message.ModifyRequest;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
 import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchResultDone;
 import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.message.UnbindRequest;
+import org.apache.directory.api.ldap.model.message.controls.SortKey;
+import org.apache.directory.api.ldap.model.message.controls.SortRequestControl;
+import org.apache.directory.api.ldap.model.message.controls.SortResponseControl;
+import org.apache.directory.api.ldap.model.message.controls.SortResponseControlImpl;
+import org.apache.directory.api.ldap.model.message.controls.SortResultCode;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.name.Rdn;
 import org.apache.directory.api.ldap.model.schema.AttributeType;
+import org.apache.directory.api.ldap.model.schema.MatchingRule;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
 import org.apache.directory.api.util.Strings;
+import org.apache.directory.mavibot.btree.BTree;
+import org.apache.directory.mavibot.btree.BTreeFactory;
+import org.apache.directory.mavibot.btree.PersistedBTreeConfiguration;
+import org.apache.directory.mavibot.btree.RecordManager;
+import org.apache.directory.mavibot.btree.exception.BTreeAlreadyManagedException;
+import org.apache.directory.mavibot.btree.serializer.StringSerializer;
 import org.apache.directory.server.constants.ServerDNConstants;
 import org.apache.directory.server.core.api.CoreSession;
 import org.apache.directory.server.core.api.DirectoryService;
@@ -1076,19 +1097,72 @@ public class DefaultCoreSession implemen
 
         OperationManager operationManager = directoryService.getOperationManager();
 
-        EntryFilteringCursor cursor = null;
+        // Check if we received serverside sort Control
+        SortRequestControl sortControl = ( SortRequestControl ) searchRequest.getControls().get( SortRequestControl.OID );
+        
+        SortResponseControl sortRespCtrl = null;
+        
+        SearchResultDone done = searchRequest.getResultResponse();
+        
+        LdapResult ldapResult = done.getLdapResult();
+        
+        if( sortControl != null )
+        {
+            sortRespCtrl = canSort( sortControl, ldapResult, getDirectoryService().getSchemaManager() );
+            
+            if ( sortControl.isCritical() && ( sortRespCtrl.getSortResult() != SortResultCode.SUCCESS ) )
+            {
+                ldapResult.setResultCode( ResultCodeEnum.UNAVAILABLE_CRITICAL_EXTENSION );
+                done.addControl( sortRespCtrl );
+                
+                return new EmptyCursor<Entry>();
+            }
+        }
+
+        Cursor<Entry> cursor = null;
 
         try
         {
             cursor = operationManager.search( searchContext );
+            
+            if ( ( sortRespCtrl != null ) && ( sortRespCtrl.getSortResult() == SortResultCode.SUCCESS ) )
+            {
+                cursor = sortResults( cursor, sortControl, getDirectoryService().getSchemaManager() );
+            }
+
+            // the below condition is to satisfy the scenario 6 in section 2 of rfc2891
+            if ( sortRespCtrl != null )
+            {
+                cursor.beforeFirst();
+                
+                if( !cursor.next() )
+                {
+                    sortRespCtrl = null;
+                }
+                else
+                {
+                    // move the cursor back
+                    cursor.previous();
+                }
+            }
         }
         catch ( LdapException e )
         {
-            searchRequest.getResultResponse().addAllControls( searchContext.getResponseControls() );
+            done.addAllControls( searchContext.getResponseControls() );
             throw e;
         }
+        catch( Exception e )
+        {
+            done.addAllControls( searchContext.getResponseControls() );
+            throw new LdapException( e );
+        }
+
+        if( sortRespCtrl != null )
+        {
+            done.addControl( sortRespCtrl );
+        }
 
-        searchRequest.getResultResponse().addAllControls( searchContext.getResponseControls() );
+        done.addAllControls( searchContext.getResponseControls() );
 
         return cursor;
     }
@@ -1116,4 +1190,156 @@ public class DefaultCoreSession implemen
         OperationManager operationManager = directoryService.getOperationManager();
         operationManager.unbind( unbindContext );
     }
+
+
+    /**
+     * Checks if the requested search results can be sorted
+     * 
+     * @param sortControl the sort control
+     * @param ldapResult the refrence to the LDAP result of the ongoing search operation
+     * @param session the current session
+     * @return a sort response control
+     */
+    private SortResponseControl canSort( SortRequestControl sortControl, LdapResult ldapResult, SchemaManager schemaManager )
+    {
+        SortResponseControl resp = new SortResponseControlImpl();
+        
+        List<SortKey> keys = sortControl.getSortKeys();
+        
+        // only ONE key is supported by the server for now
+        if( keys.size() > 1 )
+        {
+            ldapResult.setDiagnosticMessage( "Cannot sort results based on more than one attribute" );
+            resp.setSortResult( SortResultCode.UNWILLINGTOPERFORM );
+            return resp;
+        }
+        
+        SortKey sk = keys.get( 0 );
+        
+        AttributeType at = schemaManager.getAttributeType( sk.getAttributeTypeDesc() );
+        
+        if ( at == null )
+        {
+           ldapResult.setDiagnosticMessage( "No attribute with the name " + sk.getAttributeTypeDesc() + " exists in the server's schema" );
+           resp.setSortResult( SortResultCode.NOSUCHATTRIBUTE );
+           resp.setAttributeName( sk.getAttributeTypeDesc() );
+           return resp;
+        }
+        
+        String mrOid = sk.getMatchingRuleId();
+        
+        if( mrOid != null )
+        {
+            MatchingRule mr = at.getOrdering();
+            
+            if( mr != null )
+            {
+                if( !mrOid.equals( mr.getOid() ) )
+                {
+                    ldapResult.setDiagnosticMessage( "Given matchingrule " + mrOid + " is not applicable for the attribute " + sk.getAttributeTypeDesc() );
+                    resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
+                    resp.setAttributeName( sk.getAttributeTypeDesc() );
+                    return resp;        
+                }
+            }
+            
+            try
+            {
+                schemaManager.lookupComparatorRegistry( mrOid );
+            }
+            catch ( LdapException e )
+            {
+                ldapResult.setDiagnosticMessage( "Given matchingrule " + mrOid + " is not supported" );
+                resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
+                resp.setAttributeName( sk.getAttributeTypeDesc() );
+                return resp;        
+            }
+        }
+        else
+        {
+            MatchingRule mr = at.getOrdering();
+            
+            if( mr == null )
+            {
+                mr = at.getEquality();
+            }
+            
+            ldapResult.setDiagnosticMessage( "Matchingrule is required for sorting by the attribute " + sk.getAttributeTypeDesc() );
+            resp.setSortResult( SortResultCode.INAPPROPRIATEMATCHING );
+            resp.setAttributeName( sk.getAttributeTypeDesc() );
+            
+            if( mr == null )
+            {
+                return resp;
+            }
+            
+            try
+            {
+                schemaManager.lookupComparatorRegistry( mr.getOid() );
+            }
+            catch ( LdapException e )
+            {
+                return resp;        
+            }
+        }
+        
+        resp.setSortResult( SortResultCode.SUCCESS );
+        
+        return resp;
+    }
+    
+    
+    /**
+     * Sorts the entries based on the given sortkey and returns the cursor
+     * 
+     * @param unsortedEntries the cursor containing un-sorted entries
+     * @param control the sort control
+     * @param schemaManager schema manager
+     * @return a cursor containing sorted entries
+     * @throws CursorException
+     * @throws LdapException
+     * @throws IOException
+     */
+    private Cursor<Entry> sortResults( Cursor<Entry> unsortedEntries, SortRequestControl control, SchemaManager schemaManager ) throws CursorException, LdapException, IOException
+    {
+        unsortedEntries.beforeFirst();
+        
+        SortKey sk = control.getSortKeys().get( 0 );
+        
+        AttributeType at = schemaManager.getAttributeType( sk.getAttributeTypeDesc() );
+        
+        SortedEntryComparator comparator = new SortedEntryComparator( at, sk.getMatchingRuleId(), sk.isReverseOrder(), schemaManager );
+        
+        SortedEntrySerializer keySerializer = new SortedEntrySerializer( comparator );
+        
+        PersistedBTreeConfiguration<Entry, String> config = new PersistedBTreeConfiguration<Entry, String>();
+        config.setName( UUID.randomUUID().toString() );
+        config.setKeySerializer( keySerializer );
+        config.setValueSerializer( new StringSerializer() );
+        
+        BTree<Entry, String> btree = BTreeFactory.createPersistedBTree( config );
+        
+        File file = File.createTempFile( btree.getName(), ".sorted-data" );
+        RecordManager recMan = new RecordManager( file.getAbsolutePath() );
+        
+        try
+        {
+            recMan.manage( btree );
+        }
+        catch( BTreeAlreadyManagedException e )
+        {
+            throw new LdapException( e );
+        }
+        
+        while( unsortedEntries.next() )
+        {
+            Entry entry = unsortedEntries.get();
+            btree.insert( entry, null );
+        }
+        
+        unsortedEntries.close();
+        
+        return new SortedEntryCursor( btree, recMan, file );
+    }
+    
 }

Added: directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryComparator.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryComparator.java?rev=1553637&view=auto
==============================================================================
--- directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryComparator.java (added)
+++ directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryComparator.java Fri Dec 27 07:30:55 2013
@@ -0,0 +1,192 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ * 
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ * 
+ */
+package org.apache.directory.server.core.shared;
+
+
+import java.util.Comparator;
+import java.util.TreeSet;
+
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.schema.AttributeType;
+import org.apache.directory.api.ldap.model.schema.LdapComparator;
+import org.apache.directory.api.ldap.model.schema.MatchingRule;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+
+/**
+ * A comparator to sort the entries as per <a href="http://tools.ietf.org/html/rfc2891">RFC 2891</a>
+ * 
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+class SortedEntryComparator implements Comparator<Entry>
+{
+
+    /** the attribute's type */
+    private AttributeType type;
+
+    /** comparator used for comparing the values of the given attribute type */
+    private Comparator comparator;
+
+    /** flag to indicate if the attribute type is multivalued */
+    private boolean multivalued;
+
+    /** flag for indicating the order of sorting */
+    private boolean reverse;
+
+    /** flag to indicate if the attribute is human readable or binary */
+    private boolean hr;
+
+
+    /**
+     * 
+     * Creates a new instance of SortedEntryComparator.
+     *
+     * @param at the attribute's type
+     * @param mrOid the OID or name of the matchingrule
+     * @param reverse flag to indicate the sort order
+     */
+    public SortedEntryComparator( AttributeType at, String mrule, boolean reverse, SchemaManager schemaManager ) throws LdapException
+    {
+        this.type = at;
+        this.reverse = reverse;
+
+        if ( !at.isSingleValued() )
+        {
+            multivalued = true;
+        }
+
+        hr = at.getSyntax().isHumanReadable();
+
+        if( mrule != null )
+        {
+            comparator = schemaManager.lookupComparatorRegistry( mrule );
+        }
+        else
+        {
+            MatchingRule mr = at.getOrdering();
+            
+            if( mr == null )
+            {
+                mr = at.getEquality();
+            }
+            
+            comparator = schemaManager.lookupComparatorRegistry( mr.getOid() );
+        }
+        
+        ( ( LdapComparator ) comparator ).setSchemaManager( schemaManager );
+    }
+
+
+    @Override
+    public int compare( Entry entry1, Entry entry2 )
+    {
+        Attribute at1 = entry1.get( type );
+
+        Attribute at2 = entry2.get( type );
+
+        // as per section 2.2 of the spec null values are considered larger
+        if ( at1 == null )
+        {
+            return ( reverse ? -1 : 1 );
+        }
+        else if ( at2 == null )
+        {
+            return ( reverse ? 1 : -1 );
+        }
+
+        Object o1 = null;
+        Object o2 = null;
+
+        if ( multivalued )
+        {
+            TreeSet ts = new TreeSet( comparator );
+
+            o1 = sortAndGetFirst( at1, ts );
+
+            ts.clear();
+            o2 = sortAndGetFirst( at2, ts );
+        }
+        else
+        {
+            Value<?> v1 = at1.get();
+            Value<?> v2 = at2.get();
+
+            if ( hr )
+            {
+                o1 = v1.getString();
+                o2 = v2.getString();
+            }
+            else
+            {
+                o1 = v1.getBytes();
+                o2 = v2.getBytes();
+            }
+        }
+
+        if( o1 == null || o2 == null )
+        {
+            System.out.println("");
+        }
+        int c = 1;
+
+        if ( reverse )
+        {
+            c = comparator.compare( o2, o1 );
+        }
+        else
+        {
+            c = comparator.compare( o1, o2 );
+        }
+
+        if ( c == 0 )
+        {
+            return 1;
+        }
+
+        return c;
+    }
+
+
+    /**
+     * sorts the values of an attribute and picks the least value
+     * 
+     * @param at the attribute
+     * @param ts the TreeSet for sorting 
+     * @return the least value among the values of the attribute
+     */
+    private Object sortAndGetFirst( Attribute at, TreeSet ts )
+    {
+        for ( Value v : at )
+        {
+            if ( hr )
+            {
+                ts.add( v.getString() );
+            }
+            else
+            {
+                ts.add( v.getBytes() );
+            }
+        }
+
+        return ts.first();
+    }
+}
\ No newline at end of file

Added: directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryCursor.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryCursor.java?rev=1553637&view=auto
==============================================================================
--- directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryCursor.java (added)
+++ directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntryCursor.java Fri Dec 27 07:30:55 2013
@@ -0,0 +1,267 @@
+/*
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *   or more contributor license agreements.  See the NOTICE file
+ *   distributed with this work for additional information
+ *   regarding copyright ownership.  The ASF licenses this file
+ *   to you under the Apache License, Version 2.0 (the
+ *   "License"); you may not use this file except in compliance
+ *   with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing,
+ *   software distributed under the License is distributed on an
+ *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *   KIND, either express or implied.  See the License for the
+ *   specific language governing permissions and limitations
+ *   under the License.
+ *
+ */
+package org.apache.directory.server.core.shared;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.directory.api.ldap.model.cursor.AbstractCursor;
+import org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.InvalidCursorPositionException;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.mavibot.btree.BTree;
+import org.apache.directory.mavibot.btree.RecordManager;
+import org.apache.directory.mavibot.btree.Tuple;
+import org.apache.directory.mavibot.btree.TupleCursor;
+import org.apache.directory.server.core.api.filtering.EntryFilter;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
+import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * TODO SortedEntryCursor.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class SortedEntryCursor extends AbstractCursor<Entry> implements EntryFilteringCursor
+{
+
+    private static final Logger LOG = LoggerFactory.getLogger( SortedEntryCursor.class );
+    
+    private TupleCursor<Entry, String> wrapped;
+
+    private Tuple<Entry, String> tuple;
+
+    private RecordManager recMan;
+
+    private File dataFile;
+    
+    public SortedEntryCursor( BTree<Entry,String> btree, RecordManager recMan, File dataFile ) throws IOException
+    {
+        this.recMan = recMan;
+        this.dataFile = dataFile;
+        wrapped = btree.browse();
+    }
+
+
+    @Override
+    public boolean available()
+    {
+        return ( tuple != null );
+    }
+
+
+    @Override
+    public void before( Entry element ) throws LdapException, CursorException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+
+    @Override
+    public void after( Entry element ) throws LdapException, CursorException
+    {
+        throw new UnsupportedOperationException();
+    }
+
+
+    @Override
+    public void beforeFirst() throws LdapException, CursorException
+    {
+        try
+        {
+            tuple = null;
+            wrapped.beforeFirst();
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+    }
+
+
+    @Override
+    public void afterLast() throws LdapException, CursorException
+    {
+        try
+        {
+            tuple = null;
+            wrapped.afterLast();
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+
+    }
+
+
+    @Override
+    public boolean first() throws LdapException, CursorException
+    {
+        try
+        {
+            wrapped.beforeFirst();
+            return next();
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+
+    }
+
+
+    @Override
+    public boolean last() throws LdapException, CursorException
+    {
+        try
+        {
+            wrapped.afterLast();
+            return previous();
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+
+    }
+
+
+    @Override
+    public boolean previous() throws LdapException, CursorException
+    {
+        try
+        {
+            tuple = wrapped.prev();
+            return true;
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+        catch ( NoSuchElementException e )
+        {
+            // ignore this is due to the call wrapped.prev()
+            // instead of doing a check like if(wrapped.hasPrev())
+        }
+
+        tuple = null;
+        return false;
+    }
+
+
+    @Override
+    public boolean next() throws LdapException, CursorException
+    {
+        try
+        {
+            tuple = wrapped.next();
+            return true;
+        }
+        catch ( IOException e )
+        {
+            throw new CursorException( e );
+        }
+        catch ( NoSuchElementException e )
+        {
+            // ignore, this is due to the call wrapped.prev()
+            // instead of doing a check like if(wrapped.hasNext())
+        }
+
+        tuple = null;
+        return false;
+    }
+
+
+    @Override
+    public Entry get() throws CursorException
+    {
+        if ( tuple == null )
+        {
+            throw new InvalidCursorPositionException();
+        }
+
+        return tuple.getKey();
+    }
+
+
+    @Override
+    public void close()
+    {
+        wrapped.close();
+        deleteFile();
+        super.close();
+    }
+
+
+    @Override
+    public void close( Exception cause )
+    {
+        wrapped.close();
+        deleteFile();
+        super.close( cause );
+    }
+
+
+    @Override
+    public boolean addEntryFilter( EntryFilter filter )
+    {
+        return false;
+    }
+
+
+    @Override
+    public List<EntryFilter> getEntryFilters()
+    {
+        return null;
+    }
+
+
+    @Override
+    public SearchOperationContext getOperationContext()
+    {
+        return null;
+    }
+
+    private void deleteFile()
+    {
+        if( recMan == null )
+        {
+            return;
+        }
+        
+        try
+        {
+            recMan.close();
+            dataFile.delete();
+        }
+        catch( IOException e )
+        {
+            LOG.warn( "Failed to delete the sorted entry data file {}", dataFile, e );
+        }
+    }
+}

Added: directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntrySerializer.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntrySerializer.java?rev=1553637&view=auto
==============================================================================
--- directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntrySerializer.java (added)
+++ directory/apacheds/trunk/core-shared/src/main/java/org/apache/directory/server/core/shared/SortedEntrySerializer.java Fri Dec 27 07:30:55 2013
@@ -0,0 +1,345 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *  
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *  
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License. 
+ *  
+ */
+package org.apache.directory.server.core.shared;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Comparator;
+
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
+import org.apache.directory.api.ldap.model.schema.AttributeType;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.mavibot.btree.serializer.AbstractElementSerializer;
+import org.apache.directory.mavibot.btree.serializer.BufferHandler;
+import org.apache.directory.server.i18n.I18n;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Serialize and deserialize a ServerEntry. 
+ * 
+ * WARNING: This serializer stores the complete DN as well (unlike other entry 
+ *          serializers which store only RDN).
+ * 
+ * <b>This class must *not* be used anywhere else other than for storing sorted entries in server.</b>
+ *  
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class SortedEntrySerializer extends AbstractElementSerializer<Entry>
+{
+    /** The serialVersionUID */
+    private static final long serialVersionUID = 1L;
+
+    /** the logger for this class */
+    private static final Logger LOG = LoggerFactory.getLogger( SortedEntrySerializer.class );
+
+    /**
+     * Speedup for logs
+     */
+    private static final boolean IS_DEBUG = LOG.isDebugEnabled();
+
+    /** The schemaManager reference */
+    private static SchemaManager schemaManager;
+
+    private SortedEntryComparator comparator;
+
+
+    /**
+     * Creates a new instance of ServerEntrySerializer.
+     * The schemaManager MUST be set explicitly set using the static {@link #setSchemaManager(SchemaManager)}
+     */
+    public SortedEntrySerializer( SortedEntryComparator comparator )
+    {
+        super( comparator );
+        this.comparator = comparator;
+    }
+
+
+    @Override
+    public Comparator<Entry> getComparator()
+    {
+        return comparator;
+    }
+
+
+    /**
+     * <p>
+     * 
+     * This is the place where we serialize entries, and all theirs
+     * elements. the reason why we don't call the underlying methods
+     * (<code>ServerAttribute.write(), Value.write()</code>) is that we need
+     * access to the registries to read back the values.
+     * <p>
+     * The structure used to store the entry is the following :
+     * <ul>
+     *   <li><b>[Dn]</b> : The entry's Rdn.</li>
+     *   <li><b>[numberAttr]</b> : the bumber of attributes. Can be 0</li>
+     *   <li>For each Attribute :
+     *     <ul>
+     *       <li><b>[attribute's oid]</b> : The attribute's OID to get back
+     *       the attributeType on deserialization</li>
+     *       <li><b>[Attribute]</b> The attribute</li>
+     *     </ul>
+     *   </li>
+     * </ul>
+     */
+    public byte[] serialize( Entry entry )
+    {
+        try
+        {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+            ObjectOutput out = new ObjectOutputStream( baos );
+
+            // First, the Dn
+            Dn dn = entry.getDn();
+
+            // Write the Dn
+            dn.writeExternal( out );
+
+            // Then the attributes.
+            out.writeInt( entry.getAttributes().size() );
+
+            // Iterate through the keys. We store the Attribute
+            // here, to be able to restore it in the readExternal :
+            // we need access to the registries, which are not available
+            // in the ServerAttribute class.
+            for ( Attribute attribute : entry.getAttributes() )
+            {
+                AttributeType attributeType = attribute.getAttributeType();
+
+                // Write the oid to be able to restore the AttributeType when deserializing
+                // the attribute
+                String oid = attributeType.getOid();
+
+                out.writeUTF( oid );
+
+                // Write the attribute
+                attribute.writeExternal( out );
+            }
+
+            out.flush();
+
+            // Note : we don't store the ObjectClassAttribute. It has already
+            // been stored as an attribute.
+
+            if ( IS_DEBUG )
+            {
+                LOG.debug( ">------------------------------------------------" );
+                LOG.debug( "Serialize " + entry );
+            }
+
+            byte[] bytes = baos.toByteArray();
+
+            return bytes;
+        }
+        catch ( Exception e )
+        {
+            throw new RuntimeException( e );
+        }
+    }
+
+
+    /**
+     *  Deserialize a Entry.
+     *  
+     *  @param bytes the byte array containing the serialized entry
+     *  @return An instance of a Entry object 
+     *  @throws IOException if we can't deserialize the Entry
+     */
+    public Entry deserialize( ByteBuffer buffer ) throws IOException
+    {
+        // read the length
+        int len = buffer.limit();
+
+        ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buffer.array(), buffer.position(), len ) );
+
+        try
+        {
+            Entry entry = new DefaultEntry( schemaManager );
+
+            // Read the Dn
+            Dn dn = new Dn( schemaManager );
+            dn.readExternal( in );
+            entry.setDn( dn );
+
+            // Read the number of attributes
+            int nbAttributes = in.readInt();
+
+            // Read the attributes
+            for ( int i = 0; i < nbAttributes; i++ )
+            {
+                // Read the attribute's OID
+                String oid = in.readUTF();
+
+                try
+                {
+                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( oid );
+
+                    // Create the attribute we will read
+                    Attribute attribute = new DefaultAttribute( attributeType );
+
+                    // Read the attribute
+                    attribute.readExternal( in );
+
+                    entry.add( attribute );
+                }
+                catch ( LdapException ne )
+                {
+                    // We weren't able to find the OID. The attribute will not be added
+                    throw new ClassNotFoundException( ne.getMessage(), ne );
+                }
+            }
+
+            buffer.position( buffer.position() + len ); // previous position + length
+
+            return entry;
+        }
+        catch ( ClassNotFoundException cnfe )
+        {
+            LOG.error( I18n.err( I18n.ERR_134, cnfe.getLocalizedMessage() ) );
+            throw new IOException( cnfe.getLocalizedMessage() );
+        }
+    }
+
+
+    @Override
+    public Entry deserialize( BufferHandler bufferHandler ) throws IOException
+    {
+        return deserialize( ByteBuffer.wrap( bufferHandler.getBuffer() ) );
+    }
+
+
+    public static void setSchemaManager( SchemaManager schemaManager )
+    {
+        SortedEntrySerializer.schemaManager = schemaManager;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Entry fromBytes( byte[] buffer ) throws IOException
+    {
+        return fromBytes( buffer, 0 );
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Entry fromBytes( byte[] buffer, int pos ) throws IOException
+    {
+        // read the length
+        int len = buffer.length - pos;
+
+        ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream( buffer, pos, len ) );
+
+        try
+        {
+            Entry entry = new DefaultEntry( schemaManager );
+
+            // Read the Dn, if any
+            byte hasDn = in.readByte();
+
+            if ( hasDn == 1 )
+            {
+                Rdn rdn = new Rdn( schemaManager );
+                rdn.readExternal( in );
+
+                try
+                {
+                    entry.setDn( new Dn( schemaManager, rdn ) );
+                }
+                catch ( LdapInvalidDnException lide )
+                {
+                    IOException ioe = new IOException( lide.getMessage() );
+                    ioe.initCause( lide );
+                    throw ioe;
+                }
+            }
+            else
+            {
+                entry.setDn( Dn.EMPTY_DN );
+            }
+
+            // Read the number of attributes
+            int nbAttributes = in.readInt();
+
+            // Read the attributes
+            for ( int i = 0; i < nbAttributes; i++ )
+            {
+                // Read the attribute's OID
+                String oid = in.readUTF();
+
+                try
+                {
+                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( oid );
+
+                    // Create the attribute we will read
+                    Attribute attribute = new DefaultAttribute( attributeType );
+
+                    // Read the attribute
+                    attribute.readExternal( in );
+
+                    entry.add( attribute );
+                }
+                catch ( LdapException ne )
+                {
+                    // We weren't able to find the OID. The attribute will not be added
+                    throw new ClassNotFoundException( ne.getMessage(), ne );
+                }
+            }
+
+            return entry;
+        }
+        catch ( ClassNotFoundException cnfe )
+        {
+            LOG.error( I18n.err( I18n.ERR_134, cnfe.getLocalizedMessage() ) );
+            throw new IOException( cnfe.getLocalizedMessage() );
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Class<?> getType()
+    {
+        return Entry.class;
+    }
+}

Modified: directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/SearchRequestHandler.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/SearchRequestHandler.java?rev=1553637&r1=1553636&r2=1553637&view=diff
==============================================================================
--- directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/SearchRequestHandler.java (original)
+++ directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/handlers/request/SearchRequestHandler.java Fri Dec 27 07:30:55 2013
@@ -431,8 +431,13 @@ public class SearchRequestHandler extend
             count++;
         }
 
-        // DO NOT WRITE THE RESPONSE - JUST RETURN IT
-        ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
+        // check if the result code is not already set
+        // the result code might be set when sort control is present
+        if( ldapResult.getResultCode() == null )
+        {
+            // DO NOT WRITE THE RESPONSE - JUST RETURN IT
+            ldapResult.setResultCode( ResultCodeEnum.SUCCESS );
+        }
 
         if ( ( count >= sizeLimit ) && ( cursor.next() ) )
         {

Modified: directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/replication/provider/SyncReplRequestHandler.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/replication/provider/SyncReplRequestHandler.java?rev=1553637&r1=1553636&r2=1553637&view=diff
==============================================================================
--- directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/replication/provider/SyncReplRequestHandler.java (original)
+++ directory/apacheds/trunk/protocol-ldap/src/main/java/org/apache/directory/server/ldap/replication/provider/SyncReplRequestHandler.java Fri Dec 27 07:30:55 2013
@@ -81,6 +81,9 @@ import org.apache.directory.api.ldap.mod
 import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.message.controls.ChangeType;
 import org.apache.directory.api.ldap.model.message.controls.ManageDsaIT;
+import org.apache.directory.api.ldap.model.message.controls.SortKey;
+import org.apache.directory.api.ldap.model.message.controls.SortRequestControl;
+import org.apache.directory.api.ldap.model.message.controls.SortRequestControlImpl;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.AttributeType;
 import org.apache.directory.api.ldap.model.url.LdapUrl;
@@ -493,6 +496,26 @@ public class SyncReplRequestHandler impl
     private void doInitialRefresh( LdapSession session, SearchRequest request ) throws Exception
     {
         PROVIDER_LOG.debug( "Starting an initial refresh" );
+
+        SortRequestControl ctrl = ( SortRequestControl ) request.getControl( SortRequestControl.OID );
+        
+        if( ctrl != null )
+        {
+            LOG.warn( "Removing the received sort control from the syncrepl search request during initial refresh" );
+            request.removeControl( ctrl );
+        }
+        
+        LOG.debug( "Adding sort control to sort the entries by entryDn attribute to preserve order of insertion" );
+        SortKey sk = new SortKey( SchemaConstants.ENTRY_DN_AT );
+        // matchingrule for "entryDn"
+        sk.setMatchingRuleId( "2.5.13.1" );
+        sk.setReverseOrder( true );
+        
+        ctrl = new SortRequestControlImpl();
+        ctrl.addSortKey( sk );
+
+        request.addControl( ctrl );
+        
         String originalFilter = request.getFilter().toString();
         InetSocketAddress address = ( InetSocketAddress ) session.getIoSession().getRemoteAddress();
         String hostName = address.getAddress().getHostName();

Added: directory/apacheds/trunk/server-integ/src/test/java/org/apache/directory/server/operations/search/SortedSearchIT.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/server-integ/src/test/java/org/apache/directory/server/operations/search/SortedSearchIT.java?rev=1553637&view=auto
==============================================================================
--- directory/apacheds/trunk/server-integ/src/test/java/org/apache/directory/server/operations/search/SortedSearchIT.java (added)
+++ directory/apacheds/trunk/server-integ/src/test/java/org/apache/directory/server/operations/search/SortedSearchIT.java Fri Dec 27 07:30:55 2013
@@ -0,0 +1,419 @@
+/*
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *   or more contributor license agreements.  See the NOTICE file
+ *   distributed with this work for additional information
+ *   regarding copyright ownership.  The ASF licenses this file
+ *   to you under the Apache License, Version 2.0 (the
+ *   "License"); you may not use this file except in compliance
+ *   with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing,
+ *   software distributed under the License is distributed on an
+ *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *   KIND, either express or implied.  See the License for the
+ *   specific language governing permissions and limitations
+ *   under the License.
+ *
+ */
+package org.apache.directory.server.operations.search;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.directory.api.ldap.model.constants.SchemaConstants;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.PresenceNode;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
+import org.apache.directory.api.ldap.model.message.SearchResultDone;
+import org.apache.directory.api.ldap.model.message.SearchResultEntry;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.message.controls.SortKey;
+import org.apache.directory.api.ldap.model.message.controls.SortRequestControl;
+import org.apache.directory.api.ldap.model.message.controls.SortRequestControlImpl;
+import org.apache.directory.api.ldap.model.message.controls.SortResponseControl;
+import org.apache.directory.api.ldap.model.message.controls.SortResultCode;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests for searching with server side sort control.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports =
+    { @CreateTransport(protocol = "LDAP") })
+@ApplyLdifFiles(
+    { "sortedsearch-test-data.ldif" })
+public class SortedSearchIT extends AbstractLdapTestUnit
+{
+    private Dn baseDn;
+
+    private ExprNode filter;
+
+    private static LdapConnection con;
+
+    private SearchRequest req;
+
+    private SortKey sk;
+
+    private SortRequestControl ctrl;
+
+
+    @Before
+    public void createConnection() throws Exception
+    {
+        if ( con == null )
+        {
+            con = new LdapNetworkConnection( "localhost", getLdapServer().getPort() );
+            con.bind( "uid=admin,ou=system", "secret" );
+            con.setTimeOut( Long.MAX_VALUE );
+        }
+
+        baseDn = new Dn( "ou=parent,ou=system" );
+        filter = new PresenceNode( "objectClass" );
+
+        req = new SearchRequestImpl();
+        req.setBase( baseDn );
+        req.setFilter( filter );
+        req.setScope( SearchScope.SUBTREE );
+        req.addAttributes( SchemaConstants.ALL_ATTRIBUTES_ARRAY );
+
+        // tests may overwrite the fields of the below SortKey instance
+        sk = new SortKey( "entryDn" );
+        ctrl = new SortRequestControlImpl();
+        ctrl.addSortKey( sk );
+        req.addControl( ctrl );
+    }
+
+
+    @AfterClass
+    public static void closeConnection() throws Exception
+    {
+        con.close();
+    }
+
+
+    /**
+     * section #2 scenario #3
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void testWithInvalidAttributeAndCriticality() throws Exception
+    {
+        sk.setAttributeTypeDesc( "Non-existing-At" );
+        ctrl.setCritical( true );
+
+        SearchCursor cursor = con.search( req );
+        assertFalse( cursor.next() );
+
+        SearchResultDone sd = cursor.getSearchResultDone();
+
+        cursor.close();
+
+        SortResponseControl resp = ( SortResponseControl ) sd.getControl( SortResponseControl.OID );
+        assertNotNull( resp );
+
+        assertEquals( SortResultCode.NOSUCHATTRIBUTE, resp.getSortResult() );
+        assertEquals( sk.getAttributeTypeDesc(), resp.getAttributeName() );
+        assertEquals( ResultCodeEnum.UNAVAILABLE_CRITICAL_EXTENSION, sd.getLdapResult().getResultCode() );
+    }
+
+
+    /**
+     * section #2 scenario #4
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testWithInvalidAttributeAndNoCriticality() throws Exception
+    {
+        sk.setAttributeTypeDesc( "Non-existing-At" );
+        ctrl.setCritical( false );
+
+        SearchCursor cursor = con.search( req );
+
+        int count = 0;
+
+        while ( cursor.next() )
+        {
+            cursor.get();
+            count++;
+        }
+
+        cursor.close();
+
+        assertEquals( 14, count );
+
+        SearchResultDone sd = cursor.getSearchResultDone();
+
+        SortResponseControl resp = ( SortResponseControl ) sd.getControl( SortResponseControl.OID );
+        assertNotNull( resp );
+
+        assertEquals( SortResultCode.NOSUCHATTRIBUTE, resp.getSortResult() );
+        assertEquals( sk.getAttributeTypeDesc(), resp.getAttributeName() );
+        assertEquals( ResultCodeEnum.SUCCESS, sd.getLdapResult().getResultCode() );
+    }
+
+
+    /**
+     * section #2 scenario #6
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void testWithInvalidFilter() throws Exception
+    {
+        req.setFilter( new PresenceNode( "mail" ) );
+
+        SearchCursor cursor = con.search( req );
+
+        assertFalse( cursor.next() );
+
+        cursor.close();
+
+        SearchResultDone sd = cursor.getSearchResultDone();
+
+        SortResponseControl resp = ( SortResponseControl ) sd.getControl( SortResponseControl.OID );
+        assertNull( resp );
+
+        assertEquals( ResultCodeEnum.SUCCESS, sd.getLdapResult().getResultCode() );
+    }
+
+
+    ///////////////////////////// Tests for section #2 scenario #5 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
+
+    @Test
+    public void testSortBySn() throws Exception
+    {
+        sk.setAttributeTypeDesc( "sn" );
+        SearchCursor cursor = con.search( req );
+
+        List<String> expectedOrder = new ArrayList<String>();
+        expectedOrder.add( "uid=person1,ou=parent,ou=system" );
+        expectedOrder.add( "uid=person2,ou=parent,ou=system" );
+        expectedOrder.add( "uid=person3,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user0,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user1,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user2,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user3,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user4,ou=grandchildren,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user5,ou=grandchildren,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user6,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user7,ou=parent,ou=system" );
+
+        int expectedCount = expectedOrder.size();
+
+        List<String> actualOrder = new ArrayList<String>();
+
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry.getDn().getName() );
+        }
+
+        cursor.close();
+
+        // remove the LAST 3 entries present in the actualOrder list, they exist on top cause they don't have "sn" attribute
+        // NOTE: there is no guaranteed order for these LAST 3 entries
+        actualOrder.remove( actualOrder.size() - 1 );
+        actualOrder.remove( actualOrder.size() - 1 );
+        actualOrder.remove( actualOrder.size() - 1 );
+
+        assertEquals( expectedCount, actualOrder.size() );
+
+        for ( int i = 0; i < expectedOrder.size(); i++ )
+        {
+            assertEquals( expectedOrder.get( i ), actualOrder.get( i ) );
+        }
+
+        // check reverse order
+        actualOrder.clear();
+
+        sk.setReverseOrder( true );
+        cursor = con.search( req );
+
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry.getDn().getName() );
+        }
+
+        cursor.close();
+
+        // remove the FIRST 3 entries present in the actualOrder list, they exist on top cause they don't have "sn" attribute
+        // NOTE: there is no guaranteed order for these FIRST 3 entries
+        actualOrder.remove( 0 );
+        actualOrder.remove( 0 );
+        actualOrder.remove( 0 );
+
+        assertEquals( expectedCount, actualOrder.size() );
+
+        expectedCount--;
+        for ( int i = expectedOrder.size() - 1; i >= 0; i-- )
+        {
+            assertEquals( expectedOrder.get( i ), actualOrder.get( expectedCount - i ) );
+        }
+    }
+    
+    // though "sn" is also multi-valued, the test data has only one value for "sn" in each entry
+    // so using "cn" for this test
+    @Test
+    public void testSortByMultiValuedAttribute() throws Exception
+    {
+        sk.setAttributeTypeDesc( "cn" );
+        SearchCursor cursor = con.search( req );
+
+        List<String> expectedOrder = new ArrayList<String>();
+        expectedOrder.add( "uid=user6,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user0,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user1,ou=parent,ou=system" );
+        expectedOrder.add( "uid=person3,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user2,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user3,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user4,ou=grandchildren,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user5,ou=grandchildren,ou=children,ou=parent,ou=system" );
+        expectedOrder.add( "uid=user7,ou=parent,ou=system" );
+        expectedOrder.add( "uid=person1,ou=parent,ou=system" );
+        expectedOrder.add( "uid=person2,ou=parent,ou=system" );
+
+        int expectedCount = expectedOrder.size();
+
+        List<String> actualOrder = new ArrayList<String>();
+
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry.getDn().getName() );
+        }
+
+        cursor.close();
+
+        // remove the LAST 3 entries present in the actualOrder list, they exist on top cause they don't have "sn" attribute
+        // NOTE: there is no guaranteed order for these LAST 3 entries
+        actualOrder.remove( actualOrder.size() - 1 );
+        actualOrder.remove( actualOrder.size() - 1 );
+        actualOrder.remove( actualOrder.size() - 1 );
+
+        assertEquals( expectedCount, actualOrder.size() );
+
+        for ( int i = 0; i < expectedOrder.size(); i++ )
+        {
+            assertEquals( expectedOrder.get( i ), actualOrder.get( i ) );
+        }
+
+        // check reverse order
+        actualOrder.clear();
+
+        sk.setReverseOrder( true );
+        cursor = con.search( req );
+
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry.getDn().getName() );
+        }
+
+        cursor.close();
+
+        // remove the FIRST 3 entries present in the actualOrder list, they exist on top cause they don't have "sn" attribute
+        // NOTE: there is no guaranteed order for these FIRST 3 entries
+        actualOrder.remove( 0 );
+        actualOrder.remove( 0 );
+        actualOrder.remove( 0 );
+
+        assertEquals( expectedCount, actualOrder.size() );
+
+        expectedCount--;
+        for ( int i = expectedOrder.size() - 1; i >= 0; i-- )
+        {
+            assertEquals( expectedOrder.get( i ), actualOrder.get( expectedCount - i ) );
+        }
+    }
+
+    
+    @Test
+    public void testSortByDn() throws Exception
+    {
+        sk.setAttributeTypeDesc( "entryDn" );
+        sk.setMatchingRuleId( "2.5.13.1" );
+        SearchCursor cursor = con.search( req );
+
+        List<Entry> actualOrder = new ArrayList<Entry>();
+
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry );
+        }
+
+        cursor.close();
+        
+        // start deleting from the first entry
+        // SHOULD succeeded if the order is as expected
+        for( int i = 0; i < actualOrder.size(); i++ )
+        {
+            con.delete( actualOrder.get( i ).getDn() );
+        }
+        
+        // now insert from the last entry, SHOULD succeed
+        for( int i = actualOrder.size() - 1; i >= 0; i-- )
+        {
+            con.add( actualOrder.get( i ) );
+        }
+        
+        actualOrder.clear();
+
+        sk.setReverseOrder( true );
+        cursor = con.search( req );
+        
+        while ( cursor.next() )
+        {
+            SearchResultEntry se = ( SearchResultEntry ) cursor.get();
+            Entry entry = se.getEntry();
+            actualOrder.add( entry );
+        }
+
+        // now delete again, this time from the end, SHOULD succeed
+        for( int i = actualOrder.size() - 1; i >= 0; i-- )
+        {
+            con.delete( actualOrder.get( i ).getDn() );
+        }
+        
+        // now insert again, but from the beginning, SHOULD succeed
+        for( int i = 0; i < actualOrder.size(); i++ )
+        {
+            con.add( actualOrder.get( i ) );
+        }
+    }
+
+}

Added: directory/apacheds/trunk/server-integ/src/test/resources/sortedsearch-test-data.ldif
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/server-integ/src/test/resources/sortedsearch-test-data.ldif?rev=1553637&view=auto
==============================================================================
--- directory/apacheds/trunk/server-integ/src/test/resources/sortedsearch-test-data.ldif (added)
+++ directory/apacheds/trunk/server-integ/src/test/resources/sortedsearch-test-data.ldif Fri Dec 27 07:30:55 2013
@@ -0,0 +1,136 @@
+dn: ou=parent,ou=system
+objectclass: top
+objectclass: organizationalUnit
+ou: parent
+
+dn: ou=children,ou=parent,ou=system
+objectclass: top
+objectclass: organizationalUnit
+ou: children
+
+dn: ou=grandchildren,ou=children,ou=parent,ou=system
+objectclass: top
+objectclass: organizationalUnit
+ou: grandchildren
+
+dn: uid=user0,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user0_user0
+sn: user0_sn
+cn: user0_cn
+cn: cn_0
+uid: user0
+
+dn: uid=user1,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user1_user1
+sn: user1_sn
+cn: user1_cn
+cn: cn_1
+uid: user1
+
+dn: uid=user2,ou=children,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user0_user0
+sn: user2_sn
+cn: user2_cn
+cn: cn_2
+uid: user0
+
+dn: uid=user3,ou=children,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user3_user3
+sn: user3_sn
+cn: user3_cn
+cn: cn_3
+uid: user3
+
+dn: uid=user4,ou=grandchildren,ou=children,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user4_user4
+sn: user4_sn
+cn: user4_cn
+cn: cn_4
+uid: user4
+
+dn: uid=user5,ou=grandchildren,ou=children,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user5_user5
+sn: user5_sn
+cn: user5_cn
+cn: cn_5
+uid: user5
+
+dn: uid=user6,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user6_user6
+sn: user6_sn
+cn: a_cn
+cn: cn_6
+uid: user6
+
+dn: uid=user7,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: user7_user7
+sn: user7_sn
+cn: user7_cn
+cn: cn_7
+uid: user7
+
+dn: uid=person1,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: person0_person0
+sn: person1_sn
+cn: person1_cn
+cn: cn_8
+uid: person1
+
+dn: uid=person2,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: person1_person1
+sn: person2_sn
+cn: person2_cn
+cn: cn_9
+uid: person2
+
+dn: uid=person3,ou=parent,ou=system
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: person2_person2
+sn: person3_sn
+cn: person3_cn
+cn: cn_10
+uid: person3
+