diff --git a/core/src/main/java/org/springframework/ldap/core/IncrementalAttributesMapper.java b/core/src/main/java/org/springframework/ldap/core/IncrementalAttributesMapper.java new file mode 100644 index 00000000..40bdf13a --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/core/IncrementalAttributesMapper.java @@ -0,0 +1,76 @@ +/* + * Copyright 2005-2010 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.core; + +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import java.util.List; + +/** + * Utility that helps with reading all attribute values from Active Directory using Incremental Retrieval of + * Multi-valued Properties. + * + * @author Mattias Hellborg Arthursson + * @since 1.3.2 + * @see Incremental Retrieval of Multi-valued Properties + * @see {@link org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper} + */ +public interface IncrementalAttributesMapper extends AttributesMapper { + /** + * Get all of the collected values for the specified attribute. + * + * @param attributeName the attribute to get values for. + * @return the collected values for the specified attribute. Will be null + * if the requested attribute has not been returned by the server (attribute did not exist). + */ + List getValues(String attributeName); + + /** + * Get all collected values for all managed attributes as an Attributes instance. + * + * @return an Attributes instance populated with all collected values. + */ + Attributes getCollectedAttributes(); + + /** + * Check whether another query iteration is required to get all values for all attributes. + * + * @return true if there are more values for at least one of the managed attributes, + * false otherwise. + */ + boolean hasMore(); + + /** + * Get properly formatted attributes for use in the next query. The attribute names included will + * include Range specifiers as needed and only the attributes that have not been retrieved in full + * will be included. + * + * @return an array of Strings to be used as input to e.g. + * {@link org.springframework.ldap.core.LdapTemplate#lookup(javax.naming.Name, String[], org.springframework.ldap.core.AttributesMapper)} + * in the next iteration. + */ + String[] getAttributesForLookup(); + + /** + * Goes through all of the attributes to record their values and figure out whether a new query iteration + * is needed to get more values. + * + * @param attributes attributes from a SearchResult. + * @return this instance. + * @throws javax.naming.NamingException + */ + Object mapFromAttributes(Attributes attributes) throws NamingException; +} diff --git a/core/src/main/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapper.java b/core/src/main/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapper.java new file mode 100644 index 00000000..f99ba9e7 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapper.java @@ -0,0 +1,434 @@ +/* + * Copyright 2005-2010 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.core.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.IncrementalAttributesMapper; +import org.springframework.ldap.core.LdapOperations; + +import javax.naming.Name; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility class that helps with reading all attribute values from Active Directory using Incremental Retrieval of + * Multi-valued Properties. + *

+ *

Example usage of this attribute mapper: + *

+ *     List values = DefaultIncrementalAttributeMapper.lookupAttributeValues(ldapTemplate, theDn, "oneAttribute");
+ *     Attributes attrs = DefaultIncrementalAttributeMapper.lookupAttributeValues(ldapTemplate, theDn, new Object[]{"oneAttribute", "anotherAttribute"});
+ * 
+ * For greater control, e.g. explicitly specifying the requested page size, create and use an instance yourself: + *
+ *
+ *      IncrementalAttributesMapper incrementalAttributeMapper = new DefaultIncrementalAttributeMapper(10, "someAttribute");
+ *      while (incrementalAttributeMapper.hasMore()) {
+ *          ldap.lookup(entrDn, incrementalAttributeMapper.getAttributesForLookup(), incrementalAttributeMapper);
+ *      }
+ *
+ *      List values = incrementalAttributeMapper.getValues("someAttribute");
+ * 
+ *

+ *

+ * NOTE: Instances of this class are highly stateful and must not be reused or shared between threads in any way. + *

+ *

+ * NOTE: Instances of this class can only be used with lookups. No support is given for searches. + *

+ * + * @author Marius Scurtescu + * @author Mattias Hellborg Arthursson + * @see Incremental Retrieval of Multi-valued Properties + * @see #lookupAttributes(org.springframework.ldap.core.LdapOperations, javax.naming.Name, String[]) + * @see #lookupAttributeValues(org.springframework.ldap.core.LdapOperations, javax.naming.Name, String) + * @since 1.3.2 + */ +public class DefaultIncrementalAttributesMapper implements AttributesMapper, IncrementalAttributesMapper { + private final static Log log = LogFactory.getLog(DefaultIncrementalAttributesMapper.class); + + private Map stateMap = new LinkedHashMap(); + private Set rangedAttributesInNextIteration = new LinkedHashSet(); + + /** + * This guy will be used when an unmapped attribute is encountered. This really should never happen, + * but this saves us a number of null checks. + */ + private final static IncrementalAttributeState NOT_FOUND_ATTRIBUTE_STATE = new IncrementalAttributeState() { + public String getRequestedAttributeName() { + throw new UnsupportedOperationException("This method should never be called"); + } + + public boolean hasMore() { + return false; + } + + public void calculateNextRange(RangeOption responseRange) { + // Nothing to do here + } + + public String getAttributeNameForQuery() { + throw new UnsupportedOperationException("This method should never be called"); + } + + public void processValues(Attributes attributes, String attributeName) throws NamingException { + // Nothing to do here + } + + public List getValues() { + return null; + } + }; + + /** + * Create an instance for the requested attribute. + * + * @param attributeName the name of the attribute that this instance handles. + * This is the attribute name that will be requested, and whose + * values are managed. + */ + public DefaultIncrementalAttributesMapper(String attributeName) { + this(RangeOption.TERMINAL_END_OF_RANGE, attributeName); + } + + /** + * Create an instance for the requested attributes. + * + * @param attributeNames the name of the attributes that this instance handles. + * These are the attribute names that will be requested, and whose + * values are managed. + */ + public DefaultIncrementalAttributesMapper(String[] attributeNames) { + this(RangeOption.TERMINAL_END_OF_RANGE, attributeNames); + } + + /** + * Create an instance for the requested attribute with a specific page size. + * + * @param pageSize the requested page size that will be included in range query attribute names. + * @param attributeName the name of the attribute that this instance handles. + * This is the attribute name that will be requested, and whose + * values are managed. + */ + public DefaultIncrementalAttributesMapper(int pageSize, String attributeName) { + this(pageSize, new String[]{attributeName}); + } + + /** + * Create an instance for the requested attributes with a specific page size. + * + * @param pageSize the requested page size that will be included in range query attribute names. + * @param attributeNames the name of the attributes that this instance handles. + * These are the attribute names that will be requested, and whose + * values are managed. + */ + public DefaultIncrementalAttributesMapper(int pageSize, String[] attributeNames) { + for (int i = 0; i < attributeNames.length; i++) { + String attributeName = attributeNames[i]; + this.stateMap.put(attributeName, new DefaultIncrementalAttributeState(attributeName, pageSize)); + this.rangedAttributesInNextIteration.add(attributeName); + } + } + + public final Object mapFromAttributes(Attributes attributes) throws NamingException { + if (!hasMore()) { + throw new IllegalStateException("No more attributes!"); + } + + // Reset the affected attributes. + rangedAttributesInNextIteration = new HashSet(); + + NamingEnumeration attributeNameEnum = attributes.getIDs(); + while (attributeNameEnum.hasMore()) { + String attributeName = (String) attributeNameEnum.next(); + + String[] attributeNameSplit = attributeName.split(";"); + IncrementalAttributeState state = getState(attributeNameSplit[0]); + if (attributeNameSplit.length == 1) { + // No range specification for this attribute + state.processValues(attributes, attributeName); + } else { + for (int i = 0; i < attributeNameSplit.length; i++) { + String option = attributeNameSplit[i]; + + RangeOption responseRange = RangeOption.parse(option); + + if (responseRange != null) { + state.processValues(attributes, attributeName); + state.calculateNextRange(responseRange); + if (state.hasMore()) { + rangedAttributesInNextIteration.add(state.getRequestedAttributeName()); + } + } + } + } + } + + return this; + } + + private IncrementalAttributeState getState(String attributeName) { + Object mappedState = stateMap.get(attributeName); + if (mappedState == null) { + log.warn("Attribute '" + attributeName + "' is not handled by this instance"); + mappedState = NOT_FOUND_ATTRIBUTE_STATE; + } + + return (IncrementalAttributeState) mappedState; + } + + public final List getValues(String attributeName) { + return getState(attributeName).getValues(); + } + + public Attributes getCollectedAttributes() { + BasicAttributes attributes = new BasicAttributes(); + + Set attributeNames = stateMap.keySet(); + for (Iterator iterator = attributeNames.iterator(); iterator.hasNext(); ) { + String attributeName = (String) iterator.next(); + + BasicAttribute oneAttribute = new BasicAttribute(attributeName); + List values = getValues(attributeName); + if (values != null) { + for (Iterator valueIterator = values.iterator(); valueIterator.hasNext(); ) { + Object oneValue = valueIterator.next(); + oneAttribute.add(oneValue); + } + } + + attributes.put(oneAttribute); + } + + return attributes; + } + + public final boolean hasMore() { + return rangedAttributesInNextIteration.size() > 0; + } + + public final String[] getAttributesForLookup() { + String[] result = new String[rangedAttributesInNextIteration.size()]; + int index = 0; + for (Iterator iterator = rangedAttributesInNextIteration.iterator(); iterator.hasNext(); ) { + String next = (String) iterator.next(); + IncrementalAttributeState state = (IncrementalAttributeState) stateMap.get(next); + result[index++] = state.getAttributeNameForQuery(); + } + + return result; + } + + /** + * Lookup all values for the specified attribute, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attribute name of the attribute to request. + * @return an Attributes instance, populated with all found values for the requested attribute. + * Never null, though the actual attribute may not be set if it was not + * set on the requested object. + */ + public static Attributes lookupAttributes(LdapOperations ldapOperations, String dn, String attribute) { + return lookupAttributes(ldapOperations, new DistinguishedName(dn), attribute); + } + + /** + * Lookup all values for the specified attributes, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attributes names of the attributes to request. + * @return an Attributes instance, populated with all found values for the requested attributes. + * Never null, though the actual attributes may not be set if they was not + * set on the requested object. + */ + public static Attributes lookupAttributes(LdapOperations ldapOperations, String dn, String[] attributes) { + return lookupAttributes(ldapOperations, new DistinguishedName(dn), attributes); + } + + /** + * Lookup all values for the specified attribute, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attribute name of the attribute to request. + * @return an Attributes instance, populated with all found values for the requested attribute. + * Never null, though the actual attribute may not be set if it was not + * set on the requested object. + */ + public static Attributes lookupAttributes(LdapOperations ldapOperations, Name dn, String attribute) { + return lookupAttributes(ldapOperations, dn, new String[]{attribute}); + } + + /** + * Lookup all values for the specified attributes, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attributes names of the attributes to request. + * @return an Attributes instance, populated with all found values for the requested attributes. + * Never null, though the actual attributes may not be set if they was not + * set on the requested object. + */ + public static Attributes lookupAttributes(LdapOperations ldapOperations, Name dn, String[] attributes) { + return loopForAllAttributeValues(ldapOperations, dn, attributes).getCollectedAttributes(); + } + + /** + * Lookup all values for the specified attribute, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attribute name of the attribute to request. + * @return a list with all attribute values found for the requested attribute. + * Never null, an empty list indicates that the attribute was not set or empty. + */ + public static List lookupAttributeValues(LdapOperations ldapOperations, String dn, String attribute) { + return lookupAttributeValues(ldapOperations, new DistinguishedName(dn), attribute); + } + + /** + * Lookup all values for the specified attribute, looping through the results incrementally if necessary. + * + * @param ldapOperations The instance to use for performing the actual lookup. + * @param dn The distinguished name of the object to find. + * @param attribute name of the attribute to request. + * @return a list with all attribute values found for the requested attribute. + * Never null, an empty list indicates that the attribute was not set or empty. + */ + public static List lookupAttributeValues(LdapOperations ldapOperations, Name dn, String attribute) { + List values = loopForAllAttributeValues(ldapOperations, dn, new String[]{attribute}).getValues(attribute); + if(values == null) { + values = Collections.emptyList(); + } + + return values; + } + + private static DefaultIncrementalAttributesMapper loopForAllAttributeValues(LdapOperations ldapOperations, Name dn, String[] attributes) { + DefaultIncrementalAttributesMapper mapper = new DefaultIncrementalAttributesMapper(attributes); + while (mapper.hasMore()) { + ldapOperations.lookup(dn, mapper.getAttributesForLookup(), mapper); + } + return mapper; + } + + /** + * This class keeps track of the state of an individual attribute in the process of collecting + * multi-value attributes using ranges. Holds the values collected thus far, the next applicable range, + * and the actual (requested) attribute name. + */ + private final static class DefaultIncrementalAttributeState implements IncrementalAttributeState { + private final String actualAttributeName; + private List values = null; + private final int pageSize; + boolean more = true; + + private RangeOption requestRange; + + private DefaultIncrementalAttributeState(String actualAttributeName, int pageSize) { + this.actualAttributeName = actualAttributeName; + this.pageSize = pageSize; + this.requestRange = new RangeOption(0, pageSize); + } + + public boolean hasMore() { + return more; + } + + public String getRequestedAttributeName() { + return actualAttributeName; + } + + public void calculateNextRange(RangeOption responseRange) { + more = requestRange.compareTo(responseRange) > 0; + + if (more) { + requestRange = responseRange.nextRange(pageSize); + } + } + + public String getAttributeNameForQuery() { + StringBuilder attributeBuilder = new StringBuilder(actualAttributeName); + + if (!(requestRange.isFullRange())) { + attributeBuilder.append(';'); + requestRange.appendTo(attributeBuilder); + } + + return attributeBuilder.toString(); + } + + public void processValues(Attributes attributes, String attributeName) throws NamingException { + Attribute attribute = attributes.get(attributeName); + NamingEnumeration valueEnum = attribute.getAll(); + + initValuesIfApplicable(); + while (valueEnum.hasMore()) { + values.add(valueEnum.next()); + } + } + + private void initValuesIfApplicable() { + if (values == null) { + values = new LinkedList(); + } + } + + public List getValues() { + if (values != null) { + return new ArrayList(values); + } else { + return null; + } + } + } + + /** + * @author Mattias Hellborg Arthursson + */ + private static interface IncrementalAttributeState { + boolean hasMore(); + + void calculateNextRange(RangeOption responseRange); + + String getAttributeNameForQuery(); + + String getRequestedAttributeName(); + + void processValues(Attributes attributes, String attributeName) throws NamingException; + + List getValues(); + } +} diff --git a/core/src/main/java/org/springframework/ldap/support/ad/RangeOption.java b/core/src/main/java/org/springframework/ldap/core/support/RangeOption.java similarity index 95% rename from core/src/main/java/org/springframework/ldap/support/ad/RangeOption.java rename to core/src/main/java/org/springframework/ldap/core/support/RangeOption.java index a08238e3..d7800f5d 100644 --- a/core/src/main/java/org/springframework/ldap/support/ad/RangeOption.java +++ b/core/src/main/java/org/springframework/ldap/core/support/RangeOption.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ldap.support.ad; +package org.springframework.ldap.core.support; import java.util.regex.Pattern; import java.util.regex.Matcher; @@ -24,10 +24,10 @@ import java.util.regex.Matcher; * * @author Marius Scurtescu * - * @see IncrementalAttributeMapper + * @see DefaultIncrementalAttributesMapper * @since 1.3.2 */ -public class RangeOption implements Comparable { +class RangeOption implements Comparable { public static final int TERMINAL_END_OF_RANGE = -1; public static final int TERMINAL_MISSING = -2; @@ -79,13 +79,12 @@ public class RangeOption implements Comparable { public String toString() { StringBuilder rangeBuilder = new StringBuilder(); - - toString(rangeBuilder); + appendTo(rangeBuilder); return rangeBuilder.toString(); } - public void toString(StringBuilder rangeBuilder) { + public void appendTo(StringBuilder rangeBuilder) { rangeBuilder.append("Range=").append(initial); if (!isTerminalMissing()) { diff --git a/core/src/main/java/org/springframework/ldap/core/support/package.html b/core/src/main/java/org/springframework/ldap/core/support/package.html index 30162db2..48f06935 100644 --- a/core/src/main/java/org/springframework/ldap/core/support/package.html +++ b/core/src/main/java/org/springframework/ldap/core/support/package.html @@ -1,7 +1,7 @@ -Support classes the core Spring LDAP package. +Support classes for the core Spring LDAP package. diff --git a/core/src/main/java/org/springframework/ldap/support/ad/AttributeValueProcessor.java b/core/src/main/java/org/springframework/ldap/support/ad/AttributeValueProcessor.java deleted file mode 100644 index b5e25618..00000000 --- a/core/src/main/java/org/springframework/ldap/support/ad/AttributeValueProcessor.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2005-2010 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.ad; - -/** - * @since 1.3.2 - * @author Marius Scurtescu - */ -public interface AttributeValueProcessor { - public void process(Object value); -} diff --git a/core/src/main/java/org/springframework/ldap/support/ad/IncrementalAttributeMapper.java b/core/src/main/java/org/springframework/ldap/support/ad/IncrementalAttributeMapper.java deleted file mode 100644 index a80fd060..00000000 --- a/core/src/main/java/org/springframework/ldap/support/ad/IncrementalAttributeMapper.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2005-2010 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.ad; - -import org.springframework.ldap.core.AttributesMapper; - -import javax.naming.directory.Attributes; -import javax.naming.directory.Attribute; -import javax.naming.NamingException; -import javax.naming.NamingEnumeration; - -/** - * Utility class that helps with reading all attribute values from Active Directory using Incremental Retrieval of - * Multi-valued Properties. - *

- *

Example usage of this attribute mapper: - *

- *     public void retrieveAttributeIncrementally(LdapTemplate ldap, LdapName entrDn, String attributeName, AttributeValueProcessor valueProcessor)
- *     {
- *         IncrementalAttributeMapper incrementalAttributeMapper = new IncrementalAttributeMapper(attributeName, valueProcessor);
- *
- *         while (incrementalAttributeMapper.hasMore())
- *         {
- *             ldap.lookup(entrDn, incrementalAttributeMapper.getAttributesArray(), incrementalAttributeMapper);
- *         }
- *     }
- * 
- * - * @author Marius Scurtescu - * @see Incremental Retrieval of Multi-valued Properties - * @since 1.3.2 - */ -public class IncrementalAttributeMapper implements AttributesMapper { - private final String attributeName; - private final AttributeValueProcessor valueProcessor; - - private boolean more = true; - private int pageSize; - private RangeOption requestRange = new RangeOption(0, pageSize); - private boolean omitFullRange = true; - - public IncrementalAttributeMapper(String attributeName, AttributeValueProcessor valueProcessor) { - this(attributeName, valueProcessor, RangeOption.TERMINAL_END_OF_RANGE); - } - - public IncrementalAttributeMapper(String attributeName, AttributeValueProcessor valueProcessor, int pageSize) { - this.attributeName = attributeName; - this.pageSize = pageSize; - this.requestRange = new RangeOption(0, pageSize); - this.valueProcessor = valueProcessor; - } - - public boolean isOmitFullRange() { - return omitFullRange; - } - - public void setOmitFullRange(boolean omitFullRange) { - this.omitFullRange = omitFullRange; - } - - public Object mapFromAttributes(Attributes attributes) throws NamingException { - if (!more) { - throw new IllegalStateException("No more attributes!"); - } - - more = false; - - NamingEnumeration attributeNameEnum = attributes.getIDs(); - - while (attributeNameEnum.hasMore()) { - String attributeName = (String) attributeNameEnum.next(); - - if (attributeName.equals(this.attributeName)) { - processValues(attributes, this.attributeName); - } else if (attributeName.startsWith(this.attributeName + ";")) { - String[] attributeNameSplit = attributeName.split(";"); - for (int i = 0; i < attributeNameSplit.length; i++) { - String option = attributeNameSplit[i]; - - RangeOption responseRange = RangeOption.parse(option); - - if (responseRange != null) { - more = requestRange.compareTo(responseRange) > 0; - - if (more) { - requestRange = responseRange.nextRange(pageSize); - } - - processValues(attributes, attributeName); - } - } - } - } - - return this; - } - - private void processValues(Attributes attributes, String attributeName) throws NamingException { - Attribute attribute = attributes.get(attributeName); - NamingEnumeration valueEnum = attribute.getAll(); - - while (valueEnum.hasMore()) { - valueProcessor.process(valueEnum.next()); - } - } - - public boolean hasMore() { - return more; - } - - public String[] getAttributesArray() { - StringBuilder attributeBuilder = new StringBuilder(attributeName); - - if (!(omitFullRange && requestRange.isFullRange())) { - attributeBuilder.append(';'); - - requestRange.toString(attributeBuilder); - } - - return new String[]{attributeBuilder.toString()}; - } -} diff --git a/core/src/main/java/org/springframework/ldap/support/ad/ListAttributeValueProcessor.java b/core/src/main/java/org/springframework/ldap/support/ad/ListAttributeValueProcessor.java deleted file mode 100644 index 2e1a6961..00000000 --- a/core/src/main/java/org/springframework/ldap/support/ad/ListAttributeValueProcessor.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2005-2010 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.ad; - -import java.util.LinkedList; -import java.util.List; - -/** - * @since 1.3.2 - * @author Marius Scurtescu - */ -public class ListAttributeValueProcessor implements AttributeValueProcessor { - private List values = new LinkedList(); - - public void process(Object value) { - values.add(value); - } - - public List getValues() { - return values; - } -} diff --git a/core/src/main/java/org/springframework/ldap/support/ad/package.html b/core/src/main/java/org/springframework/ldap/support/ad/package.html deleted file mode 100644 index ff67bc21..00000000 --- a/core/src/main/java/org/springframework/ldap/support/ad/package.html +++ /dev/null @@ -1,7 +0,0 @@ - - - -Support classes for Active Directory-specific functionality. - - - diff --git a/core/src/main/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorder.java b/core/src/main/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorder.java index 12ddd7ab..3d83f455 100644 --- a/core/src/main/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorder.java +++ b/core/src/main/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorder.java @@ -15,22 +15,22 @@ */ package org.springframework.ldap.transaction.compensating; -import java.util.HashSet; -import java.util.Set; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapOperations; +import org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper; +import org.springframework.ldap.core.IncrementalAttributesMapper; +import org.springframework.transaction.compensating.CompensatingTransactionOperationExecutor; +import org.springframework.transaction.compensating.CompensatingTransactionOperationRecorder; +import org.springframework.util.Assert; import javax.naming.Name; -import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.directory.DirContext; import javax.naming.directory.ModificationItem; - -import org.springframework.ldap.core.AttributesMapper; -import org.springframework.ldap.core.LdapOperations; -import org.springframework.transaction.compensating.CompensatingTransactionOperationExecutor; -import org.springframework.transaction.compensating.CompensatingTransactionOperationRecorder; -import org.springframework.util.Assert; +import java.util.HashSet; +import java.util.Set; /** * A {@link CompensatingTransactionOperationRecorder} keeping track of @@ -71,8 +71,16 @@ public class ModifyAttributesOperationRecorder implements // Get the current values of all referred Attributes. String[] attributeNameArray = (String[]) set.toArray(new String[set .size()]); - Attributes currentAttributes = (Attributes) ldapOperations.lookup(dn, - attributeNameArray, getAttributesMapper()); + + // LDAP-234: We need to explicitly an IncrementalAttributesMapper in + // case we're working against AD and there are too many attribute values to be returned + // by one query. + IncrementalAttributesMapper attributesMapper = getAttributesMapper(attributeNameArray); + while (attributesMapper.hasMore()) { + ldapOperations.lookup(dn, attributesMapper.getAttributesForLookup(), attributesMapper); + } + + Attributes currentAttributes = attributesMapper.getCollectedAttributes(); // Get a compensating ModificationItem for each of the incoming // modification. @@ -93,13 +101,8 @@ public class ModifyAttributesOperationRecorder implements * @return the {@link AttributesMapper} to use for getting the current * Attributes of the target DN. */ - AttributesMapper getAttributesMapper() { - return new AttributesMapper() { - public Object mapFromAttributes(Attributes attributes) - throws NamingException { - return attributes; - } - }; + IncrementalAttributesMapper getAttributesMapper(String[] attributeNames) { + return new DefaultIncrementalAttributesMapper(attributeNames); } /** diff --git a/core/src/test/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapperTest.java b/core/src/test/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapperTest.java new file mode 100644 index 00000000..ffc646e6 --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/core/support/DefaultIncrementalAttributesMapperTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2005-2010 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.core.support; + +import junit.framework.TestCase; +import org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper; +import org.springframework.ldap.core.support.RangeOption; + +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; + +/** + * IncrementalAttributesMapper Tester. + * + * @author Marius Scurtescu + * @author Mattias Hellborg Arthursson + */ +public class DefaultIncrementalAttributesMapperTest extends TestCase { + private DefaultIncrementalAttributesMapper tested; + + public DefaultIncrementalAttributesMapperTest(String name) { + super(name); + } + + public void setUp() throws Exception { + tested = new DefaultIncrementalAttributesMapper("member"); + } + + public void tearDown() throws Exception { + tested = null; + } + + public void testGetAttributesArray() throws Exception { + String[] attributes = tested.getAttributesForLookup(); + + assertEquals(1, attributes.length); + assertEquals("member", attributes[0]); + + tested = new DefaultIncrementalAttributesMapper(10, "member"); + + attributes = tested.getAttributesForLookup(); + + assertEquals(1, attributes.length); + assertEquals("member;Range=0-10", attributes[0]); + } + + public void testGetAttributesArrayWithTwoAttributes() { + tested = new DefaultIncrementalAttributesMapper(20, new String[]{"member", "cn"}); + String[] attributes = tested.getAttributesForLookup(); + + assertEquals(2, attributes.length); + + assertEquals("member;Range=0-20", attributes[0]); + assertEquals("cn;Range=0-20", attributes[1]); + } + + public void testLoopEmpty() throws Exception { + assertTrue(tested.hasMore()); + + Attributes attributes = new BasicAttributes(); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertNull(tested.getValues("member")); + } + + public void testLoop() throws Exception { + Attributes attributes = createAttributes("member", new RangeOption(0, 10)); + + tested.mapFromAttributes(attributes); + + assertTrue(tested.hasMore()); + assertEquals(11, tested.getValues("member").size()); + + attributes = createAttributes("member", new RangeOption(11), 5); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertEquals(16, tested.getValues("member").size()); + } + + public void test1LoopWithPageSizeExact() throws Exception { + tested = new DefaultIncrementalAttributesMapper(10, "member"); + + Attributes attributes = createAttributes("member", new RangeOption(0, 10)); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertEquals(11, tested.getValues("member").size()); + } + + public void test2LoopsWithPageSizeExact() throws Exception { + tested = new DefaultIncrementalAttributesMapper(20, "member"); + + Attributes attributes = createAttributes("member", new RangeOption(0, 10)); + + tested.mapFromAttributes(attributes); + + assertTrue(tested.hasMore()); + assertEquals(11, tested.getValues("member").size()); + + attributes = createAttributes("member", new RangeOption(11, 30)); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertEquals(31, tested.getValues("member").size()); + } + + public void test2LoopsWithPageSize() throws Exception { + tested = new DefaultIncrementalAttributesMapper(20, "member"); + + Attributes attributes = createAttributes("member", new RangeOption(0, 10)); + + tested.mapFromAttributes(attributes); + + assertTrue(tested.hasMore()); + assertEquals(11, tested.getValues("member").size()); + + attributes = createAttributes("member", new RangeOption(11), 5); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertEquals(16, tested.getValues("member").size()); + } + + public void testLoopWithTwoRangedAttributesLoopOnOneAttribute() throws Exception { + tested = new DefaultIncrementalAttributesMapper(10, new String[]{"member", "cn"}); + + Attributes attributes = createAttributes("member", new RangeOption(0, 5)); + attributes.put(createRangeAttribute("cn", new RangeOption(0, 10), 10)); + + tested.mapFromAttributes(attributes); + + assertTrue(tested.hasMore()); + assertEquals(6, tested.getValues("member").size()); + assertEquals(10, tested.getValues("cn").size()); + + assertEquals(1, tested.getAttributesForLookup().length); + + attributes = createAttributes("member", new RangeOption(6), 5); + + tested.mapFromAttributes(attributes); + + assertFalse(tested.hasMore()); + assertEquals(11, tested.getValues("member").size()); + } + private Attributes createAttributes(String attributeName, RangeOption range) { + return createAttributes(attributeName, range, range.getTerminal() - range.getInitial() + 1); + } + + private Attributes createAttributes(String attributeName, RangeOption range, int valueCnt) { + Attributes attributes = new BasicAttributes(); + + Attribute attribute = createRangeAttribute(attributeName, range, valueCnt); + attributes.put(attribute); + + return attributes; + + } + + private Attribute createRangeAttribute(String attributeName, RangeOption range, int valueCnt) { + Attribute attribute = new BasicAttribute(attributeName + ";" + range.toString()); + for (int i = 0; i < valueCnt; i++) { + attribute.add("value" + (range.getInitial() + i - 1)); + } + return attribute; + } +} diff --git a/core/src/test/java/org/springframework/ldap/support/ad/RangeOptionTest.java b/core/src/test/java/org/springframework/ldap/core/support/RangeOptionTest.java similarity index 97% rename from core/src/test/java/org/springframework/ldap/support/ad/RangeOptionTest.java rename to core/src/test/java/org/springframework/ldap/core/support/RangeOptionTest.java index 31428355..f8e5e05b 100644 --- a/core/src/test/java/org/springframework/ldap/support/ad/RangeOptionTest.java +++ b/core/src/test/java/org/springframework/ldap/core/support/RangeOptionTest.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ldap.support.ad; +package org.springframework.ldap.core.support; import junit.framework.TestCase; +import org.springframework.ldap.core.support.RangeOption; /** - * IncrementalAttributeMapper Tester. + * IncrementalAttributesMapper Tester. * * @author Marius Scurtescu */ diff --git a/core/src/test/java/org/springframework/ldap/support/ad/IncrementalAttributeMapperTest.java b/core/src/test/java/org/springframework/ldap/support/ad/IncrementalAttributeMapperTest.java deleted file mode 100644 index 882eec3a..00000000 --- a/core/src/test/java/org/springframework/ldap/support/ad/IncrementalAttributeMapperTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2005-2010 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.ad; - -import junit.framework.TestCase; - -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; - -/** - * IncrementalAttributeMapper Tester. - * - * @author Marius Scurtescu - */ -public class IncrementalAttributeMapperTest extends TestCase { - private IncrementalAttributeMapper incrementalAttributeMapper; - private ListAttributeValueProcessor valueProcessor; - - public IncrementalAttributeMapperTest(String name) { - super(name); - } - - public void setUp() throws Exception { - valueProcessor = new ListAttributeValueProcessor(); - incrementalAttributeMapper = new IncrementalAttributeMapper("member", valueProcessor); - } - - public void tearDown() throws Exception { - incrementalAttributeMapper = null; - valueProcessor = null; - } - - public void testGetAttributesArray() throws Exception { - String[] attributes = incrementalAttributeMapper.getAttributesArray(); - - assertEquals(1, attributes.length); - assertEquals("member", attributes[0]); - - incrementalAttributeMapper = new IncrementalAttributeMapper("member", valueProcessor, 10); - - attributes = incrementalAttributeMapper.getAttributesArray(); - - assertEquals(1, attributes.length); - assertEquals("member;Range=0-10", attributes[0]); - } - - public void testLoopEmpty() throws Exception { - assertTrue(incrementalAttributeMapper.hasMore()); - - Attributes attributes = new BasicAttributes(); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertFalse(incrementalAttributeMapper.hasMore()); - assertEquals(0, valueProcessor.getValues().size()); - } - - public void testLoop() throws Exception { - Attributes attributes = createAttributes(new RangeOption(0, 10), true); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertTrue(incrementalAttributeMapper.hasMore()); - assertEquals(11, valueProcessor.getValues().size()); - - attributes = createAttributes(new RangeOption(11), 5, false); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertFalse(incrementalAttributeMapper.hasMore()); - assertEquals(16, valueProcessor.getValues().size()); - } - - public void test1LoopWithPageSizeExact() throws Exception { - incrementalAttributeMapper = new IncrementalAttributeMapper("member", valueProcessor, 10); - - Attributes attributes = createAttributes(new RangeOption(0, 10), true); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertFalse(incrementalAttributeMapper.hasMore()); - assertEquals(11, valueProcessor.getValues().size()); - } - - public void test2LoopsWithPageSizeExact() throws Exception { - incrementalAttributeMapper = new IncrementalAttributeMapper("member", valueProcessor, 20); - - Attributes attributes = createAttributes(new RangeOption(0, 10), true); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertTrue(incrementalAttributeMapper.hasMore()); - assertEquals(11, valueProcessor.getValues().size()); - - attributes = createAttributes(new RangeOption(11, 30), false); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertFalse(incrementalAttributeMapper.hasMore()); - assertEquals(31, valueProcessor.getValues().size()); - } - - public void test2LoopsWithPageSize() throws Exception { - incrementalAttributeMapper = new IncrementalAttributeMapper("member", valueProcessor, 20); - - Attributes attributes = createAttributes(new RangeOption(0, 10), true); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertTrue(incrementalAttributeMapper.hasMore()); - assertEquals(11, valueProcessor.getValues().size()); - - attributes = createAttributes(new RangeOption(11), 5, false); - - incrementalAttributeMapper.mapFromAttributes(attributes); - - assertFalse(incrementalAttributeMapper.hasMore()); - assertEquals(16, valueProcessor.getValues().size()); - } - - private Attributes createAttributes(RangeOption range, boolean emptyPlain) { - return createAttributes(range, range.getTerminal() - range.getInitial() + 1, emptyPlain); - } - - private Attributes createAttributes(RangeOption range, int valueCnt, boolean emptyPlain) { - Attributes attributes = new BasicAttributes(); - - if (emptyPlain) { - attributes.put(new BasicAttribute("member")); - } - - Attribute attribute = new BasicAttribute("member;" + range.toString()); - for (int i = 0; i < valueCnt; i++) { - attribute.add("value" + (range.getInitial() + i - 1)); - } - attributes.put(attribute); - - return attributes; - - } -} diff --git a/core/src/test/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorderTest.java b/core/src/test/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorderTest.java index afe8a7c3..0d8ae53b 100644 --- a/core/src/test/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorderTest.java +++ b/core/src/test/java/org/springframework/ldap/transaction/compensating/ModifyAttributesOperationRecorderTest.java @@ -15,6 +15,13 @@ */ package org.springframework.ldap.transaction.compensating; +import junit.framework.TestCase; +import org.easymock.MockControl; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapOperations; +import org.springframework.ldap.core.IncrementalAttributesMapper; +import org.springframework.transaction.compensating.CompensatingTransactionOperationExecutor; + import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; @@ -23,16 +30,6 @@ import javax.naming.directory.BasicAttributes; import javax.naming.directory.DirContext; import javax.naming.directory.ModificationItem; -import junit.framework.TestCase; - -import org.easymock.MockControl; -import org.springframework.ldap.core.AttributesMapper; -import org.springframework.ldap.core.DistinguishedName; -import org.springframework.ldap.core.LdapOperations; -import org.springframework.ldap.transaction.compensating.ModifyAttributesOperationExecutor; -import org.springframework.ldap.transaction.compensating.ModifyAttributesOperationRecorder; -import org.springframework.transaction.compensating.CompensatingTransactionOperationExecutor; - public class ModifyAttributesOperationRecorderTest extends TestCase { private MockControl ldapOperationsControl; @@ -40,7 +37,7 @@ public class ModifyAttributesOperationRecorderTest extends TestCase { private MockControl attributesMapperControl; - private AttributesMapper attributesMapperMock; + private IncrementalAttributesMapper attributesMapperMock; private ModifyAttributesOperationRecorder tested; @@ -49,8 +46,8 @@ public class ModifyAttributesOperationRecorderTest extends TestCase { ldapOperationsMock = (LdapOperations) ldapOperationsControl.getMock(); attributesMapperControl = MockControl - .createControl(AttributesMapper.class); - attributesMapperMock = (AttributesMapper) attributesMapperControl + .createControl(IncrementalAttributesMapper.class); + attributesMapperMock = (IncrementalAttributesMapper) attributesMapperControl .getMock(); tested = new ModifyAttributesOperationRecorder(ldapOperationsMock); @@ -79,14 +76,14 @@ public class ModifyAttributesOperationRecorderTest extends TestCase { public void testRecordOperation() { final ModificationItem incomingItem = new ModificationItem( DirContext.ADD_ATTRIBUTE, new BasicAttribute("attribute1")); - ModificationItem[] incomingMods = new ModificationItem[] { incomingItem }; + ModificationItem[] incomingMods = new ModificationItem[]{incomingItem}; final ModificationItem compensatingItem = new ModificationItem( DirContext.ADD_ATTRIBUTE, new BasicAttribute("attribute2")); final Attributes expectedAttributes = new BasicAttributes(); tested = new ModifyAttributesOperationRecorder(ldapOperationsMock) { - AttributesMapper getAttributesMapper() { + IncrementalAttributesMapper getAttributesMapper(String[] attributeNames) { return attributesMapperMock; } @@ -101,14 +98,24 @@ public class ModifyAttributesOperationRecorderTest extends TestCase { DistinguishedName expectedName = new DistinguishedName("cn=john doe"); ldapOperationsControl.setDefaultMatcher(MockControl.ARRAY_MATCHER); - ldapOperationsControl.expectAndReturn(ldapOperationsMock.lookup( - expectedName, new String[] { "attribute1" }, - attributesMapperMock), expectedAttributes); + + attributesMapperControl.expectAndReturn( + attributesMapperMock.hasMore(), true); + attributesMapperControl.expectAndReturn( + attributesMapperMock.getAttributesForLookup(), + new String[]{"attribute1"}); + ldapOperationsControl.expectAndReturn( + ldapOperationsMock.lookup(expectedName, new String[]{"attribute1"}, attributesMapperMock), + expectedAttributes); + attributesMapperControl.expectAndReturn(attributesMapperMock.hasMore(), false); + attributesMapperControl.expectAndReturn( + attributesMapperMock.getCollectedAttributes(), + expectedAttributes); replay(); // Perform test CompensatingTransactionOperationExecutor operation = tested - .recordOperation(new Object[] { expectedName, incomingMods }); + .recordOperation(new Object[]{expectedName, incomingMods}); verify(); // Verify outcome diff --git a/settings.gradle b/settings.gradle index f60e1219..44e4a90e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,10 +13,11 @@ include 'samples/demos/demo' include 'samples/demos/demo-tiger' include 'samples/samples-utils' include 'samples/simple-odm' +include 'test-support' include 'test/integration-tests' include 'test/integration-tests-openldap' include 'test/integration-tests-sunone' -include 'test-support' +include 'test/integration-tests-ad' rootProject.children.each { p-> def name = p.name diff --git a/src/docbkx/index.xml b/src/docbkx/index.xml index 02c50711..4cb586d7 100644 --- a/src/docbkx/index.xml +++ b/src/docbkx/index.xml @@ -50,4 +50,5 @@ + diff --git a/src/docbkx/utilities.xml b/src/docbkx/utilities.xml new file mode 100644 index 00000000..5fe85dd6 --- /dev/null +++ b/src/docbkx/utilities.xml @@ -0,0 +1,24 @@ + + Utilities + + Incremental Retrieval of Multi-Valued Attributes + + When there are a very large number of attribute values (>1500) for a specific attribute, + Active Directory will typically refuse to return all these values at once. Instead + the attribute values will be returned according to the + Incremental Retrieval of Multi-valued Properties + method. This requires the calling part to inspect the returned attribute for specific markers and, + if necessary, make additional lookup requests until all values are found. + + + Spring LDAP's org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper + helps working with this kind of attributes, as follows: + +Attributes attrs = DefaultIncrementalAttributeMapper.lookupAttributes(ldapTemplate, theDn, new Object[]{"oneAttribute", "anotherAttribute"}); + + This will parse any returned attribute range markers and make repeated requests as necessary until all values + for all requested attributes have been retrieved. + + + + \ No newline at end of file diff --git a/test/integration-tests-ad/build.gradle b/test/integration-tests-ad/build.gradle new file mode 100644 index 00000000..225c69bc --- /dev/null +++ b/test/integration-tests-ad/build.gradle @@ -0,0 +1,14 @@ +apply from: JAVA_SCRIPT + +dependencies { + compile project(":spring-ldap-test"), + project(":spring-ldap-core-tiger") + + provided "org.springframework:spring-jdbc:$springVersion", + "org.springframework:spring-orm:$springVersion" + + testCompile "org.springframework:spring-test:$springVersion", + "junit:junit:$junitVersion" +} + +test.enabled = false // TODO this should be enabled depending on build parameter \ No newline at end of file diff --git a/test/integration-tests-ad/src/test/java/org/springframework/ldap/itest/ad/IncrementalAttributeMapperITest.java b/test/integration-tests-ad/src/test/java/org/springframework/ldap/itest/ad/IncrementalAttributeMapperITest.java new file mode 100644 index 00000000..c6092802 --- /dev/null +++ b/test/integration-tests-ad/src/test/java/org/springframework/ldap/itest/ad/IncrementalAttributeMapperITest.java @@ -0,0 +1,210 @@ +package org.springframework.ldap.itest.ad; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.SizeLimitExceededException; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import java.io.UnsupportedEncodingException; +import java.util.List; + +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 static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Mattias Hellborg Arthursson + */ +@ContextConfiguration("classpath:/incrementalAttributeMapperTest.xml") +public class IncrementalAttributeMapperITest extends AbstractJUnit4SpringContextTests { + + private static final DistinguishedName BASE_DN = new DistinguishedName("ou=dummy,dc=261consulting,dc=local"); + private static final DistinguishedName OU_DN = new DistinguishedName("ou=dummy"); + private static final DistinguishedName GROUP_DN = new DistinguishedName(OU_DN).append("cn", "testgroup"); + private static final String DEFAULT_PASSWORD = "ahcoophah5Oi4oh"; + + @Autowired + private LdapTemplate ldapTemplate; + + @Autowired + private PlatformTransactionManager transactionManager; + + @Before + public void prepareTestData() throws UnsupportedEncodingException { + cleanup(); + + createBaseOu(); + for (int i = 0; i < 1502; i++) { + createUser("test" + i); + } + + createGroup(); + } + + private void createGroup() { + DirContextAdapter ctx = new DirContextAdapter(GROUP_DN); + + ctx.addAttributeValue("objectclass", "top"); + ctx.addAttributeValue("objectclass", "group"); + ctx.addAttributeValue("cn", "testgroup"); + ctx.addAttributeValue("sAMAccountName", "TESTGROUP"); + + for (int i = 0; i < 1501; i++) { + ctx.addAttributeValue("member", buildUserRefDn("test" + i)); + } + + ldapTemplate.bind(ctx); + } + + private String buildUserRefDn(String username) { + return new DistinguishedName(BASE_DN).append("cn", username).toString(); + } + + private void createBaseOu() { + createOu(); + } + + private void createUser(String username) throws UnsupportedEncodingException { + DirContextAdapter ctx = new DirContextAdapter(new DistinguishedName(OU_DN).append("cn", username)); + + ctx.addAttributeValue("objectclass", "top"); + ctx.addAttributeValue("objectclass", "person"); + ctx.addAttributeValue("objectclass", "organizationalPerson"); + ctx.addAttributeValue("objectclass", "user"); + + ctx.setAttributeValue("givenName", username); + ctx.setAttributeValue("userPrincipalName", username + "@example.com"); + ctx.setAttributeValue("cn", username); + ctx.setAttributeValue("description", "Dummy user"); + ctx.setAttributeValue("sAMAccountName", username.toUpperCase() + "." + username.toUpperCase()); + ctx.setAttributeValue("userAccountControl", "512"); + + String newQuotedPassword = "\"" + DEFAULT_PASSWORD + "\""; + ctx.setAttributeValue("unicodePwd", newQuotedPassword.getBytes("UTF-16LE")); + + ldapTemplate.bind(ctx); + } + + private void createOu() { + DirContextAdapter ctx = new DirContextAdapter(OU_DN); + + ctx.addAttributeValue("objectClass", "top"); + ctx.addAttributeValue("objectClass", "organizationalUnit"); + + ctx.setAttributeValue("ou", "dummy"); + ctx.setAttributeValue("description", "dummy description"); + + ldapTemplate.bind(ctx); + } + + @After + public void cleanup() { + try { + ldapTemplate.lookup(OU_DN); + } catch (NameNotFoundException e) { + // Nothing to cleanup + return; + } + + while (true) { + try { + ldapTemplate.unbind(OU_DN, true); + // Everything is deleted + return; + } catch (SizeLimitExceededException e) { + // There's more to delete + } + } + } + + + @Test + public void verifyRetrievalOfLotsOfAttributeValues() { + DistinguishedName testgroupDn = new DistinguishedName(OU_DN).append("cn", "testgroup"); + + // The 'member' attribute consists of > 1500 entries and will not be returned without range specifier. + DirContextOperations ctx = ldapTemplate.lookupContext(testgroupDn); + assertNull(ctx.getStringAttribute("member")); + + DefaultIncrementalAttributesMapper attributeMapper = new DefaultIncrementalAttributesMapper(new String[]{"member", "cn"}); + assertTrue("There should be more results to get", attributeMapper.hasMore()); + + String[] attributesArray = attributeMapper.getAttributesForLookup(); + assertEquals(2, attributesArray.length); + assertEquals("member", attributesArray[0]); + assertEquals("cn", attributesArray[1]); + + // First iteration - there should now be more members left, but all cn values should have been collected. + ldapTemplate.lookup(testgroupDn, attributesArray, attributeMapper); + + assertTrue("There should be more results to get", attributeMapper.hasMore()); + // Only member attribute should be requested in this query. + attributesArray = attributeMapper.getAttributesForLookup(); + assertEquals(1, attributesArray.length); + assertEquals("member;Range=1500-*", attributesArray[0]); + + // Second iteration - all data should now have been collected. + ldapTemplate.lookup(testgroupDn, attributeMapper.getAttributesForLookup(), attributeMapper); + assertFalse("There should be no more results to get", attributeMapper.hasMore()); + + List memberValues = attributeMapper.getValues("member"); + assertNotNull(memberValues); + assertEquals(1501, memberValues.size()); + + List cnValues = attributeMapper.getValues("cn"); + assertNotNull(cnValues); + assertEquals(1, cnValues.size()); + } + + @Test + public void JiraLdap234ITest() { + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + + try { + transactionTemplate.execute(new TransactionCallback() { + @Override + public Object doInTransaction(TransactionStatus status) { + ModificationItem modificationItem = new ModificationItem( + DirContext.ADD_ATTRIBUTE, + new BasicAttribute("member", buildUserRefDn("test" + 1501))); + ldapTemplate.modifyAttributes(GROUP_DN, new ModificationItem[]{modificationItem}); + + // The below should cause a rollback + throw new RuntimeException("Simulate some failure"); + } + }); + + fail("RuntimeException expected"); + } catch (RuntimeException expected) { + DefaultIncrementalAttributesMapper attributeMapper = new DefaultIncrementalAttributesMapper(new String[]{"member"}); + while (attributeMapper.hasMore()) { + ldapTemplate.lookup(GROUP_DN, attributeMapper.getAttributesForLookup(), attributeMapper); + } + + // LDAP-234: After rollback the attribute values were cleared after rollback + assertEquals( + 1501, + DefaultIncrementalAttributesMapper.lookupAttributeValues( + ldapTemplate, GROUP_DN, "member").size()); + } + } +} diff --git a/test/integration-tests-ad/src/test/resources/incrementalAttributeMapperTest.xml b/test/integration-tests-ad/src/test/resources/incrementalAttributeMapperTest.xml new file mode 100644 index 00000000..28f6e9ab --- /dev/null +++ b/test/integration-tests-ad/src/test/resources/incrementalAttributeMapperTest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/integration-tests-openldap/build.gradle b/test/integration-tests-openldap/build.gradle index 127431c8..e6572394 100644 --- a/test/integration-tests-openldap/build.gradle +++ b/test/integration-tests-openldap/build.gradle @@ -16,4 +16,4 @@ dependencies { "com.sun:ldapbp:1.0" } -test.enabled = false // FIXME this needs to be enabled (error due to missing host) \ No newline at end of file +test.enabled = false // TODO this should be enabled depending on build parameter \ No newline at end of file diff --git a/test/integration-tests-sunone/build.gradle b/test/integration-tests-sunone/build.gradle index ec892041..650dc54f 100644 --- a/test/integration-tests-sunone/build.gradle +++ b/test/integration-tests-sunone/build.gradle @@ -16,4 +16,4 @@ dependencies { "gsbase:gsbase:$gsbaseVersion" } -test.enabled = false // FIXME this needs to be enabled (error due to missing host) \ No newline at end of file +test.enabled = false // TODO this should be enabled depending on build parameter \ No newline at end of file