From 20729ba640d5449b85c48bdad9dd4b23d4518efd Mon Sep 17 00:00:00 2001 From: Ulrik Sandberg Date: Sat, 20 Jan 2007 00:08:24 +0000 Subject: [PATCH] Added SortControlDirContextProcessor to sandbox, because no sort support in openldap. --- sandbox/.classpath | 106 ++++----- .../conf/ldapTemplateTestContext-openldap.xml | 3 +- .../LdapTemplateSortedSearchITest.java | 143 ++++++++++++ ...dapTemplateVirtualListViewSearchITest.java | 5 +- .../SortControlDirContextProcessor.java | 217 ++++++++++++++++++ .../VirtualListViewRequestControl.java | 4 + .../SortControlDirContextProcessorTest.java | 192 ++++++++++++++++ 7 files changed, 614 insertions(+), 56 deletions(-) create mode 100644 sandbox/src/itest-openldap/java/org/springframework/ldap/control/LdapTemplateSortedSearchITest.java rename sandbox/src/itest-openldap/java/org/springframework/ldap/{ => control}/LdapTemplateVirtualListViewSearchITest.java (95%) create mode 100644 sandbox/src/main/java/org/springframework/ldap/support/control/SortControlDirContextProcessor.java create mode 100644 sandbox/src/test/java/org/springframework/ldap/control/SortControlDirContextProcessorTest.java 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(); + } +}