diff --git a/core/src/main/java/org/springframework/ldap/control/AbstractFallbackRequestAndResponseControlDirContextProcessor.java b/core/src/main/java/org/springframework/ldap/control/AbstractFallbackRequestAndResponseControlDirContextProcessor.java new file mode 100644 index 00000000..98c3d902 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/control/AbstractFallbackRequestAndResponseControlDirContextProcessor.java @@ -0,0 +1,190 @@ +package org.springframework.ldap.control; + +import java.lang.reflect.Constructor; +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.UncategorizedLdapException; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Convenient base class useful when implementing a standard DirContextProcessor + * which has a request control and a response control. It handles the loading of + * the control classes, using fallback implementations specified by the subclass + * if necessary. It handles the request control constructor invocation; it only + * needs the constructor arguments to be provided. It also handles most of the + * work in the post processing of the response control, only delegating to a + * template method for the actual value retrieval. In short, it makes it easy to + * implement a custom DirContextProcessor.

+ * + *
+ * public class SortControlDirContextProcessor extends AbstractFallbackRequestAndResponseControlDirContextProcessor {
+ * 	String sortKey;
+ * 
+ * 	private boolean sorted = false;
+ * 
+ * 	private int resultCode = -1;
+ * 
+ * 	public SortControlDirContextProcessor(String sortKey) {
+ * 		this.sortKey = sortKey;
+ * 
+ * 		defaultRequestControl = "javax.naming.ldap.SortControl";
+ * 		defaultResponseControl = "com.sun.jndi.ldap.ctl.SortControl";
+ * 		fallbackRequestControl = "javax.naming.ldap.SortResponseControl";
+ * 		fallbackResponseControl = "com.sun.jndi.ldap.ctl.SortResponseControl";
+ * 
+ * 		loadControlClasses();
+ * 	}
+ * 
+ * 	public boolean isSorted() {
+ * 		return sorted;
+ * 	}
+ * 
+ * 	public int getResultCode() {
+ * 		return resultCode;
+ * 	}
+ * 
+ * 	public Control createRequestControl() {
+ * 		return super.createRequestControl(new Class[] { String[].class, boolean.class }, new Object[] {
+ *				new String[] { sortKey }, Boolean.valueOf(critical) });
+ * 	}
+ * 
+ * 	protected void handleResponse(Object control) {
+ * 		Boolean result = (Boolean) invokeMethod("isSorted", responseControlClass, control);
+ * 		this.sorted = result.booleanValue();
+ * 		Integer code = (Integer) invokeMethod("getResultCode", responseControlClass, control);
+ * 		resultCode = code.intValue();
+ * 	}
+ * }
+ * 
+ * + * @author Ulrik Sandberg + */ +public abstract class AbstractFallbackRequestAndResponseControlDirContextProcessor extends + AbstractRequestControlDirContextProcessor { + + private static final boolean CRITICAL_CONTROL = true; + + protected Class responseControlClass; + + protected Class requestControlClass; + + protected boolean critical = CRITICAL_CONTROL; + + protected String defaultRequestControl; + + protected String defaultResponseControl; + + protected String fallbackRequestControl; + + protected String fallbackResponseControl; + + protected void loadControlClasses() { + Assert.notNull(defaultRequestControl, "defaultRequestControl must not be null"); + Assert.notNull(defaultResponseControl, "defaultResponseControl must not be null"); + Assert.notNull(fallbackRequestControl, "fallbackRequestControl must not be null"); + Assert.notNull(fallbackResponseControl, "fallbackReponseControl must not be null"); + try { + requestControlClass = Class.forName(defaultRequestControl); + responseControlClass = Class.forName(defaultResponseControl); + } + catch (ClassNotFoundException e) { + log.debug("Default control classes not found - falling back to LdapBP classes", e); + + try { + requestControlClass = Class.forName(fallbackRequestControl); + responseControlClass = Class.forName(fallbackResponseControl); + } + catch (ClassNotFoundException e1) { + throw new UncategorizedLdapException( + "Neither default nor fallback classes are available - unable to proceed", e); + } + } + } + + /** + * Set the class of the expected ResponseControl for the sorted result + * response. + * + * @param responseControlClass Class of the expected response control. + */ + public void setResponseControlClass(Class responseControlClass) { + this.responseControlClass = responseControlClass; + } + + public void setRequestControlClass(Class requestControlClass) { + this.requestControlClass = requestControlClass; + } + + /** + * Utility method for invoking a method on a Control. + * @param method name of method to invoke + * @param clazz Class of the object that the method should be invoked on + * @param control Instance that the method should be invoked on + * @return the invocation result, if any + */ + protected Object invokeMethod(String method, Class clazz, Object control) { + Method actualMethod = ReflectionUtils.findMethod(clazz, method); + return ReflectionUtils.invokeMethod(actualMethod, control); + } + + /** + * Creates a request control using the constructor parameters given in + * params. + * @param paramTypes Types of the constructor parameters + * @param params Actual constructor parameters + * @return Control to be used by the DirContextProcessor + */ + public Control createRequestControl(Class[] paramTypes, Object[] params) { + Constructor constructor = ClassUtils.getConstructorIfAvailable(requestControlClass, paramTypes); + if (constructor == null) { + throw new IllegalArgumentException("Failed to find an appropriate RequestControl constructor"); + } + + Control result = null; + try { + result = (Control) constructor.newInstance(params); + } + catch (Exception e) { + ReflectionUtils.handleReflectionException(e); + } + + return result; + } + + /* + * @see + * org.springframework.ldap.core.DirContextProcessor#postProcess(javax.naming + * .directory.DirContext) + */ + public void postProcess(DirContext ctx) throws NamingException { + + LdapContext ldapContext = (LdapContext) ctx; + Control[] responseControls = ldapContext.getResponseControls(); + if (responseControls == null) { + responseControls = new Control[0]; + } + + // 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 (responseControl.getClass().isAssignableFrom(responseControlClass)) { + Object control = responseControl; + handleResponse(control); + return; + } + } + + log.fatal("No matching response control found for paged results - looking for '" + responseControlClass); + } + + protected abstract void handleResponse(Object control); +} diff --git a/core/src/main/java/org/springframework/ldap/control/PagedResultsDirContextProcessor.java b/core/src/main/java/org/springframework/ldap/control/PagedResultsDirContextProcessor.java new file mode 100644 index 00000000..73c99062 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/control/PagedResultsDirContextProcessor.java @@ -0,0 +1,136 @@ +/* + * Copyright 2005-2008 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 javax.naming.ldap.Control; + +/** + * DirContextProcessor implementation for managing the paged results control. + * Note that due to the internal workings of LdapTemplate, the + * target connection is closed after each LDAP call. The PagedResults control + * require the same connection be used for each call, which means we need to + * make sure the target connection is never actually closed. There's basically + * two ways of making this happen: use the SingleContextSource + * implementation or make sure all calls happen within a single LDAP transaction + * (using ContextSourceTransactionManager). + * + * @author Mattias Hellborg Arthursson + * @author Ulrik Sandberg + */ +public class PagedResultsDirContextProcessor extends AbstractFallbackRequestAndResponseControlDirContextProcessor { + + private static final String DEFAULT_REQUEST_CONTROL = "javax.naming.ldap.PagedResultsControl"; + + private static final String FALLBACK_REQUEST_CONTROL = "com.sun.jndi.ldap.ctl.PagedResultsControl"; + + private static final String DEFAULT_RESPONSE_CONTROL = "javax.naming.ldap.PagedResultsResponseControl"; + + private static final String FALLBACK_RESPONSE_CONTROL = "com.sun.jndi.ldap.ctl.PagedResultsResponseControl"; + + private int pageSize; + + private PagedResultsCookie cookie; + + private int resultSize; + + /** + * Constructs a new instance. This constructor should be used when + * performing the first paged search operation, when no other results have + * been retrieved. + * + * @param pageSize the page size. + */ + public PagedResultsDirContextProcessor(int pageSize) { + this(pageSize, null); + } + + /** + * Constructs a new instance with the supplied page size and cookie. The + * cookie must be the exact same instance as received from a previous paged + * results search, or null if it is the first in an operation + * sequence. + * + * @param pageSize the page size. + * @param cookie the cookie, as received from a previous search. + */ + public PagedResultsDirContextProcessor(int pageSize, PagedResultsCookie cookie) { + this.pageSize = pageSize; + this.cookie = cookie; + + defaultRequestControl = DEFAULT_REQUEST_CONTROL; + defaultResponseControl = DEFAULT_RESPONSE_CONTROL; + fallbackRequestControl = FALLBACK_REQUEST_CONTROL; + fallbackResponseControl = FALLBACK_RESPONSE_CONTROL; + + loadControlClasses(); + } + + /** + * Get the cookie. + * + * @return the cookie. + */ + public PagedResultsCookie getCookie() { + return cookie; + } + + /** + * Get the page size. + * + * @return the page size. + */ + public int getPageSize() { + return pageSize; + } + + /** + * Get the total estimated number of entries that matches the issued search. + * Note that this value is optional for the LDAP server to return, so it + * does not always contain any valid data. + * + * @return the estimated result size, if returned from the server. + */ + public int getResultSize() { + return resultSize; + } + + /* + * @see + * org.springframework.ldap.control.AbstractRequestControlDirContextProcessor + * #createRequestControl() + */ + public Control createRequestControl() { + byte[] actualCookie = null; + if (cookie != null) { + actualCookie = cookie.getCookie(); + } + return super.createRequestControl(new Class[] { int.class, byte[].class, boolean.class }, + new Object[] {new Integer(pageSize), actualCookie, Boolean.valueOf(critical)}); + } + + /* + * @seeorg.springframework.ldap.control. + * AbstractFallbackRequestAndResponseControlDirContextProcessor + * #handleResponse(java.lang.Object) + */ + protected void handleResponse(Object control) { + byte[] result = (byte[]) invokeMethod("getCookie", responseControlClass, control); + this.cookie = new PagedResultsCookie(result); + Integer wrapper = (Integer) invokeMethod("getResultSize", responseControlClass, control); + this.resultSize = wrapper.intValue(); + } +}