diff --git a/sandbox/.classpath b/sandbox/.classpath
index 7b974334..d64123a4 100644
--- a/sandbox/.classpath
+++ b/sandbox/.classpath
@@ -1,53 +1,53 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sandbox/src/itest-openldap/java/conf/ldapTemplateTestContext-openldap.xml b/sandbox/src/itest-openldap/java/conf/ldapTemplateTestContext-openldap.xml
index df48a532..a74892f3 100644
--- a/sandbox/src/itest-openldap/java/conf/ldapTemplateTestContext-openldap.xml
+++ b/sandbox/src/itest-openldap/java/conf/ldapTemplateTestContext-openldap.xml
@@ -13,7 +13,8 @@
-
+
+
diff --git a/sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateSortedSearchITest.java b/sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateSortedSearchITest.java
new file mode 100644
index 00000000..c4af7888
--- /dev/null
+++ b/sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateSortedSearchITest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.ldap.control;
+
+import java.util.List;
+
+import javax.naming.Name;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.SearchControls;
+
+import org.springframework.ldap.Person;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.CollectingNameClassPairCallbackHandler;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.SearchExecutor;
+import org.springframework.ldap.support.control.SortControlDirContextProcessor;
+import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
+
+/**
+ * Tests the still experimental sorted search result capability of LdapTemplate.
+ *
+ * @author Ulrik Sandberg
+ */
+public class LdapTemplateSortedSearchITest extends
+ AbstractDependencyInjectionSpringContextTests {
+
+ private static final Name BASE = DistinguishedName.EMPTY_PATH;
+
+ private static final String FILTER_STRING = "(&(objectclass=ikeaperson)(cn=gor*))";
+
+ private LdapTemplate tested;
+
+ private CollectingNameClassPairCallbackHandler callbackHandler;
+
+ private SearchControls searchControls;
+
+ protected String[] getConfigLocations() {
+ return new String[] { "/conf/ldapTemplateTestContext-openldap.xml" };
+ }
+
+ protected void onSetUp() throws Exception {
+ super.onSetUp();
+ PersonAttributesMapper mapper = new PersonAttributesMapper();
+ callbackHandler = tested.new AttributesMapperCallbackHandler(mapper);
+ searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ }
+
+ protected void onTearDown() throws Exception {
+ super.onTearDown();
+ callbackHandler = null;
+ tested = null;
+ searchControls = null;
+ }
+
+ public void testSearch_SortControl() {
+ SearchExecutor searchExecutor = new SearchExecutor() {
+ public NamingEnumeration executeSearch(DirContext ctx)
+ throws NamingException {
+ return ctx.search(BASE, FILTER_STRING, searchControls);
+ }
+ };
+ SortControlDirContextProcessor requestControl;
+
+ // Prepare for first search
+ requestControl = new SortControlDirContextProcessor("cn");
+ tested.search(searchExecutor, callbackHandler, requestControl);
+ int resultCode = requestControl.getResultCode();
+ boolean sorted = requestControl.isSorted();
+ assertTrue("Search result should have been sorted: " + resultCode, sorted);
+ List list = callbackHandler.getList();
+ assertSortedList(list);
+ }
+
+ public void testSearch_SortControl_ConvenienceMethod() {
+ SortControlDirContextProcessor requestControl;
+
+ // Prepare for first search
+ requestControl = new SortControlDirContextProcessor("cn");
+ tested.search(BASE, FILTER_STRING, searchControls, callbackHandler,
+ requestControl);
+ int resultCode = requestControl.getResultCode();
+ boolean sorted = requestControl.isSorted();
+ assertTrue("Search result should have been sorted: " + resultCode, sorted);
+ List list = callbackHandler.getList();
+ assertSortedList(list);
+ }
+
+ private void assertSortedList(List list) {
+ Person person;
+ assertEquals(6, list.size());
+ person = (Person) list.get(0);
+ assertEquals("Goran Milenkovic", person.getFullname());
+ person = (Person) list.get(1);
+ assertEquals("Goran Sundberg", person.getFullname());
+ person = (Person) list.get(2);
+ assertEquals("Goran Westerberg", person.getFullname());
+ person = (Person) list.get(3);
+ assertEquals("Gorana Milicevic", person.getFullname());
+ person = (Person) list.get(4);
+ assertEquals("Gordana Canic", person.getFullname());
+ person = (Person) list.get(5);
+ assertEquals("Gordana Russ", person.getFullname());
+ }
+
+ public void setTested(LdapTemplate tested) {
+ this.tested = tested;
+ }
+
+ private class PersonAttributesMapper implements AttributesMapper {
+
+ /**
+ * Maps the given attributes into a {@link Person} object.
+ *
+ * @see org.springframework.ldap.core.AttributesMapper#mapFromAttributes(javax.naming.directory.Attributes)
+ */
+ public Object mapFromAttributes(Attributes attributes)
+ throws NamingException {
+ Person person = new Person();
+ person.setFullname((String) attributes.get("cn").get());
+ person.setLastname((String) attributes.get("sn").get());
+ person.setDescription((String) attributes.get("givenName").get());
+ return person;
+ }
+ }
+}
diff --git a/sandbox/src/itest-openldap/java/org/springframework/ldap/LdapTemplateVirtualListViewSearchITest.java b/sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateVirtualListViewSearchITest.java
similarity index 95%
rename from sandbox/src/itest-openldap/java/org/springframework/ldap/LdapTemplateVirtualListViewSearchITest.java
rename to sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateVirtualListViewSearchITest.java
index 4e746da1..b27cc5a4 100644
--- a/sandbox/src/itest-openldap/java/org/springframework/ldap/LdapTemplateVirtualListViewSearchITest.java
+++ b/sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateVirtualListViewSearchITest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.ldap;
+package org.springframework.ldap.control;
import java.util.List;
@@ -21,6 +21,7 @@ import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
+import org.springframework.ldap.Person;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.CollectingNameClassPairCallbackHandler;
import org.springframework.ldap.core.LdapTemplate;
@@ -37,7 +38,7 @@ import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
public class LdapTemplateVirtualListViewSearchITest extends
AbstractDependencyInjectionSpringContextTests {
- private static final String BASE_STRING = "o=ikea.com";
+ private static final String BASE_STRING = "";
private static final String FILTER_STRING = "(&(objectclass=ikeaperson))";
diff --git a/sandbox/src/main/java/org/springframework/ldap/support/control/SortControlDirContextProcessor.java b/sandbox/src/main/java/org/springframework/ldap/support/control/SortControlDirContextProcessor.java
new file mode 100644
index 00000000..82163664
--- /dev/null
+++ b/sandbox/src/main/java/org/springframework/ldap/support/control/SortControlDirContextProcessor.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.ldap.support.control;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.ldap.Control;
+import javax.naming.ldap.LdapContext;
+
+import org.springframework.ldap.control.AbstractRequestControlDirContextProcessor;
+import org.springframework.ldap.control.CreateControlFailedException;
+import org.springframework.util.ReflectionUtils;
+
+import com.sun.jndi.ldap.ctl.SortControl;
+import com.sun.jndi.ldap.ctl.SortResponseControl;
+
+/**
+ * DirContextProcessor implementation for managing the {@link SortControl}.
+ *
+ * @author Ulrik Sandberg
+ */
+public class SortControlDirContextProcessor extends
+ AbstractRequestControlDirContextProcessor {
+
+ private static final Class DEFAULT_RESPONSE_CONTROL = SortResponseControl.class;
+
+ private static final boolean CRITICAL_CONTROL = true;
+
+ private static final String JAVA5_RESPONSE_CONTROL = "javax.naming.ldap.SortResponseControl";
+
+ /**
+ * What key to sort on.
+ */
+ private String sortKey;
+
+ /**
+ * Whether the search result actually was sorted.
+ */
+ private boolean sorted;
+
+ /**
+ * The result code of the supposedly sorted search.
+ */
+ private int resultCode;
+
+ private Class responseControlClass = DEFAULT_RESPONSE_CONTROL;
+
+ private Class fallbackResponseControlClass;
+
+ private Class currentResponseControlClass;
+
+ public SortControlDirContextProcessor(String sortKey) {
+ this.sortKey = sortKey;
+ fallbackResponseControlClass = loadFallbackResponseControlClass();
+ setSorted(false);
+ setResultCode(-1);
+ }
+
+ /**
+ * Set the class of the expected ResponseControl for the sorted result
+ * response. The default is {@link SortResponseControl}.
+ *
+ * @param responseControlClass
+ * Class of the expected response control.
+ */
+ public void setResponseControlClass(Class responseControlClass) {
+ this.responseControlClass = responseControlClass;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+
+ private void setSorted(boolean sorted) {
+ this.sorted = sorted;
+ }
+
+ public int getResultCode() {
+ return resultCode;
+ }
+
+ private void setResultCode(int sortResult) {
+ this.resultCode = sortResult;
+ }
+
+ public String getSortKey() {
+ return sortKey;
+ }
+
+ public void setSortKey(String sortKey) {
+ this.sortKey = sortKey;
+ }
+
+ /*
+ * @see org.springframework.ldap.control.AbstractRequestControlDirContextProcessor#createRequestControl()
+ */
+ public Control createRequestControl() {
+ try {
+ return new SortControl(new String[] { sortKey }, CRITICAL_CONTROL);
+ } catch (IOException e) {
+ throw new CreateControlFailedException(
+ "Error creating SortControl", e);
+ }
+ }
+
+ /*
+ * @see org.springframework.ldap.core.DirContextProcessor#postProcess(javax.naming.directory.DirContext)
+ */
+ public void postProcess(DirContext ctx) throws NamingException {
+ // initialize from property
+ currentResponseControlClass = responseControlClass;
+
+ LdapContext ldapContext = (LdapContext) ctx;
+ Control[] responseControls = ldapContext.getResponseControls();
+
+ if (responseControls == null) {
+ return;
+ }
+
+ // Go through response controls and get info, regardless of class
+ for (int i = 0; i < responseControls.length; i++) {
+ Control responseControl = responseControls[i];
+
+ // check for match, try fallback otherwise
+ if (isSortResponseControl(responseControl)) {
+ Object control = responseControl;
+ Boolean result = (Boolean) invokeMethod("isSorted",
+ currentResponseControlClass, control);
+ setSorted(result.booleanValue());
+ Integer code = (Integer) invokeMethod("getResultCode",
+ currentResponseControlClass, control);
+ setResultCode(code.intValue());
+ }
+ }
+ }
+
+ /**
+ * Check if the given control matches a sort response control. Try the
+ * fallback class from Java5 if there is no match. Set the
+ * {@link #currentResponseControlClass} to the fallback if it matches.
+ *
+ * @param responseControl
+ * the control to check for a match
+ * @return whether the control is a paged results response control
+ */
+ private boolean isSortResponseControl(Control responseControl) {
+ if (responseControl.getClass().isAssignableFrom(
+ currentResponseControlClass)) {
+ return true;
+ }
+ if (fallbackResponseControlClass != null
+ && responseControl.getClass().isAssignableFrom(
+ fallbackResponseControlClass)) {
+ currentResponseControlClass = fallbackResponseControlClass;
+ return true;
+ }
+ return false;
+ }
+
+ private Class loadFallbackResponseControlClass() {
+ Class fallbackResponseControlClass = null;
+ try {
+ fallbackResponseControlClass = Class
+ .forName(JAVA5_RESPONSE_CONTROL);
+ } catch (ClassNotFoundException e) {
+ log.debug("Could not load Java5 response control class "
+ + JAVA5_RESPONSE_CONTROL);
+ }
+ return fallbackResponseControlClass;
+ }
+
+ private Object invokeMethod(String method, Class clazz, Object control) {
+ // For Spring 2.0 ReflectionUtils could be used for all of this, but
+ // since we still want to support the 1.2 branch we do it manually and
+ // only use the stuff present in 1.2.8.
+ Method actualMethod = null;
+ Object retval = null;
+ try {
+ actualMethod = clazz.getMethod(method, new Class[0]);
+ } catch (SecurityException e) {
+ ReflectionUtils.handleReflectionException(e);
+ } catch (NoSuchMethodException e) {
+ ReflectionUtils.handleReflectionException(e);
+ }
+
+ try {
+ retval = actualMethod.invoke(control, new Object[0]);
+ } catch (IllegalArgumentException e) {
+ ReflectionUtils.handleReflectionException(e);
+ } catch (IllegalAccessException e) {
+ ReflectionUtils.handleReflectionException(e);
+ } catch (InvocationTargetException e) {
+ ReflectionUtils.handleReflectionException(e);
+ }
+
+ // Retval will be set unless an exception has been thrown.
+ return retval;
+ }
+}
diff --git a/sandbox/src/main/java/org/springframework/ldap/support/control/VirtualListViewRequestControl.java b/sandbox/src/main/java/org/springframework/ldap/support/control/VirtualListViewRequestControl.java
index e7af114e..a2c593fa 100644
--- a/sandbox/src/main/java/org/springframework/ldap/support/control/VirtualListViewRequestControl.java
+++ b/sandbox/src/main/java/org/springframework/ldap/support/control/VirtualListViewRequestControl.java
@@ -173,6 +173,10 @@ public class VirtualListViewRequestControl implements DirContextProcessor {
LdapContext ldapContext = (LdapContext) ctx;
Control[] responseControls = ldapContext.getResponseControls();
+ if (responseControls == null) {
+ return;
+ }
+
// Go through response controls and get info, regardless of class
for (int i = 0; i < responseControls.length; i++) {
Control responseControl = responseControls[i];
diff --git a/sandbox/src/test/java/org/springframework/ldap/control/SortControlDirContextProcessorTest.java b/sandbox/src/test/java/org/springframework/ldap/control/SortControlDirContextProcessorTest.java
new file mode 100644
index 00000000..68e8c6f9
--- /dev/null
+++ b/sandbox/src/test/java/org/springframework/ldap/control/SortControlDirContextProcessorTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2002-2005 the original author or authors.
+ *
+ * Licensed 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.springframework.ldap.control;
+
+import java.io.IOException;
+
+import javax.naming.ldap.Control;
+import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.PagedResultsControl;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.springframework.ldap.support.control.SortControlDirContextProcessor;
+
+import com.sun.jndi.ldap.Ber;
+import com.sun.jndi.ldap.BerDecoder;
+import com.sun.jndi.ldap.BerEncoder;
+import com.sun.jndi.ldap.ctl.DirSyncResponseControl;
+import com.sun.jndi.ldap.ctl.SortControl;
+import com.sun.jndi.ldap.ctl.SortResponseControl;
+
+/**
+ * Unit tests for the SortControlDirContextProcessor class.
+ * {@link javax.naming.ldap.SortControl}
+ * {@link javax.naming.ldap.SortResponseControl}
+ * {@link PagedResultsControl}
+ *
+ * @author Ulrik Sandberg
+ */
+public class SortControlDirContextProcessorTest extends TestCase {
+
+ private MockControl ldapContextControl;
+
+ private LdapContext ldapContextMock;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // Create ldapContext mock
+ ldapContextControl = MockControl.createControl(LdapContext.class);
+ ldapContextMock = (LdapContext) ldapContextControl.getMock();
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ ldapContextControl = null;
+ ldapContextMock = null;
+ }
+
+ protected void replay() {
+ ldapContextControl.replay();
+ }
+
+ protected void verify() {
+ ldapContextControl.verify();
+ }
+
+ public void testCreateRequestControl() throws Exception {
+ SortControlDirContextProcessor tested = new SortControlDirContextProcessor(
+ "key");
+
+ SortControl control = (SortControl) tested.createRequestControl();
+ assertNotNull(control);
+ }
+
+ public void testPostProcess() throws Exception {
+ byte sortResult = 0; // success
+
+ byte[] value = encodeValue(sortResult);
+ SortResponseControl control = new SortResponseControl(
+ "dummy", true, value);
+
+ ldapContextControl.expectAndDefaultReturn(ldapContextMock
+ .getResponseControls(), new Control[] { control });
+
+ SortControlDirContextProcessor tested = new SortControlDirContextProcessor("key");
+
+ replay();
+
+ tested.postProcess(ldapContextMock);
+
+ verify();
+
+ assertEquals(true, tested.isSorted());
+ assertEquals(0, tested.getResultCode());
+ }
+
+ public void testPostProcess_NonSuccess() throws Exception {
+ byte sortResult = 1;
+
+ byte[] value = encodeValue(sortResult);
+ SortResponseControl control = new SortResponseControl(
+ "dummy", true, value);
+
+ ldapContextControl.expectAndDefaultReturn(ldapContextMock
+ .getResponseControls(), new Control[] { control });
+
+ SortControlDirContextProcessor tested = new SortControlDirContextProcessor("key");
+
+ replay();
+
+ tested.postProcess(ldapContextMock);
+
+ verify();
+
+ assertEquals(false, tested.isSorted());
+ assertEquals(1, tested.getResultCode());
+ }
+
+ public void testPostProcess_InvalidResponseControl() throws Exception {
+ int resultSize = 50;
+ byte pageSize = 8;
+
+ byte[] value = new byte[1];
+ value[0] = pageSize;
+ byte[] cookie = encodeDirSyncValue(resultSize, value);
+
+ // Using another response control to verify that it is ignored
+ DirSyncResponseControl control = new DirSyncResponseControl("dummy",
+ true, cookie);
+
+ ldapContextControl.expectAndDefaultReturn(ldapContextMock
+ .getResponseControls(), new Control[] { control });
+
+ SortControlDirContextProcessor tested = new SortControlDirContextProcessor("key");
+
+ replay();
+
+ tested.postProcess(ldapContextMock);
+
+ verify();
+
+ assertEquals(false, tested.isSorted());
+ }
+
+ public void testBerDecoding() throws Exception {
+ int sortResult = 53; // unwilling to perform
+ byte[] encoded = encodeValue(sortResult);
+
+ BerDecoder ber = new BerDecoder(encoded, 0, encoded.length);
+
+ ber.parseSeq(null);
+ int actualSortResult = ber.parseEnumeration();
+
+ assertEquals("sortResult,", 53, actualSortResult);
+ }
+
+ private byte[] encodeValue(int sortResult) throws IOException {
+
+ // build the ASN.1 encoding
+ BerEncoder ber = new BerEncoder(10);
+
+ ber.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
+ ber.encodeInt(sortResult, Ber.ASN_ENUMERATED);
+ ber.endSeq();
+
+ return ber.getTrimmedBuf();
+ }
+
+ /**
+ * Encode a value suitable for the DirSyncResponseControl used in a test.
+ */
+ private byte[] encodeDirSyncValue(int pageSize, byte[] cookie)
+ throws IOException {
+
+ // build the ASN.1 encoding
+ BerEncoder ber = new BerEncoder(10 + cookie.length);
+
+ ber.beginSeq(Ber.ASN_SEQUENCE | Ber.ASN_CONSTRUCTOR);
+ ber.encodeInt(1); // flag
+ ber.encodeInt(pageSize); // maxReturnLength
+ ber.encodeOctetString(cookie, Ber.ASN_OCTET_STR);
+ ber.endSeq();
+
+ return ber.getTrimmedBuf();
+ }
+}