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
+