diff --git a/core/src/main/java/org/springframework/ldap/core/LdapOperations.java b/core/src/main/java/org/springframework/ldap/core/LdapOperations.java index 9b19577b..8006dff7 100644 --- a/core/src/main/java/org/springframework/ldap/core/LdapOperations.java +++ b/core/src/main/java/org/springframework/ldap/core/LdapOperations.java @@ -20,6 +20,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.ContextNotEmptyException; import org.springframework.ldap.NamingException; import org.springframework.ldap.core.support.AbstractContextSource; +import org.springframework.ldap.query.LdapQuery; import org.springframework.ldap.support.LdapUtils; import javax.naming.Binding; @@ -1503,7 +1504,39 @@ public interface LdapOperations { */ T searchForObject(Name base, String filter, ContextMapper mapper); - /** + /** + * Perform a search for a unique entry matching the specified search + * criteria and return the found object. If no entry is found or if there + * are more than one matching entry, an + * {@link IncorrectResultSizeDataAccessException} is thrown. + * @param base the DN to use as the base of the search. + * @param filter the search filter. + * @param searchControls the searchControls to use for the search. + * @param mapper the mapper to use for the search. + * @return the single object returned by the mapper that matches the search + * criteria. + * @throws IncorrectResultSizeDataAccessException if the result is not one unique entry + * @since 2.0 + */ + T searchForObject(Name base, String filter, SearchControls searchControls, ContextMapper mapper); + + /** + * Perform a search for a unique entry matching the specified search + * criteria and return the found object. If no entry is found or if there + * are more than one matching entry, an + * {@link IncorrectResultSizeDataAccessException} is thrown. + * @param base the DN to use as the base of the search. + * @param filter the search filter. + * @param searchControls the searchControls to use for the search. + * @param mapper the mapper to use for the search. + * @return the single object returned by the mapper that matches the search + * criteria. + * @throws IncorrectResultSizeDataAccessException if the result is not one unique entry + * @since 2.0 + */ + T searchForObject(String base, String filter, SearchControls searchControls, ContextMapper mapper); + + /** * Perform a search for a unique entry matching the specified search * criteria and return the found object. If no entry is found or if there * are more than one matching entry, an @@ -1517,4 +1550,57 @@ public interface LdapOperations { * @since 1.3 */ T searchForObject(String base, String filter, ContextMapper mapper); + + /** + * Perform a search with parameters from the specified LdapQuery. All found objects will be supplied to the + * ContextMapper for processing, and all returned objects will be collected in a list to be returned. + * + * @param query the LDAP query specification. + * @param mapper the ContextMapper to supply all found entries to. + * @return a List containing all entries received from the + * ContextMapper. + * + * @throws NamingException if any error occurs. + * @since 2.0 + */ + List search(LdapQuery query, ContextMapper mapper); + + /** + * Perform a search with parameters from the specified LdapQuery. The Attributes of the found entrieswill be supplied to the + * AttributesMapper for processing, and all returned objects will be collected in a list to be returned. + * + * @param query the LDAP query specification. + * @param mapper the Attributes to supply all found Attributes to. + * @return a List containing all entries received from the + * Attributes. + * + * @throws NamingException if any error occurs. + * @since 2.0 + */ + List search(LdapQuery query, AttributesMapper mapper); + + /** + * Perform a search for a unique entry matching the specified LDAP + * query and return the found entry as a DirContextOperation instance. If no entry is found or if there + * are more than one matching entry, an + * {@link IncorrectResultSizeDataAccessException} is thrown. + * @param query the DN to use as the base of the search. + * @return the single entry matching the query as a DirContextOperations instance. + * @throws IncorrectResultSizeDataAccessException if the result is not one unique entry + * @since 2.0 + */ + DirContextOperations searchForContext(LdapQuery query); + + /** + * Perform a search for a unique entry matching the specified LDAP + * query and return the found object. If no entry is found or if there + * are more than one matching entry, an + * {@link IncorrectResultSizeDataAccessException} is thrown. + * @param query the LDAP query specification. + * @return the single object returned by the mapper that matches the search + * criteria. + * @throws IncorrectResultSizeDataAccessException if the result is not one unique entry + * @since 2.0 + */ + T searchForObject(LdapQuery query, ContextMapper mapper); } diff --git a/core/src/main/java/org/springframework/ldap/core/LdapTemplate.java b/core/src/main/java/org/springframework/ldap/core/LdapTemplate.java index 5574cc5c..88f17f58 100644 --- a/core/src/main/java/org/springframework/ldap/core/LdapTemplate.java +++ b/core/src/main/java/org/springframework/ldap/core/LdapTemplate.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.NamingException; +import org.springframework.ldap.query.LdapQuery; import org.springframework.ldap.support.LdapUtils; import org.springframework.util.Assert; @@ -1527,15 +1528,10 @@ public class LdapTemplate implements LdapOperations, InitializingBean { * .Name, java.lang.String, org.springframework.ldap.core.ContextMapper) */ public T searchForObject(Name base, String filter, ContextMapper mapper) { - List result = search(base, filter, mapper); - if (result.size() == 0) { - throw new EmptyResultDataAccessException(1); - } - else if (result.size() != 1) { - throw new IncorrectResultSizeDataAccessException(1, result.size()); - } - - return result.get(0); + return searchForObject(base, + filter, + getDefaultSearchControls(defaultSearchScope, RETURN_OBJ_FLAG, ALL_ATTRIBUTES), + mapper); } /* @@ -1549,7 +1545,25 @@ public class LdapTemplate implements LdapOperations, InitializingBean { return searchForObject(LdapUtils.newLdapName(base), filter, mapper); } - private static final class NullAuthenticatedLdapEntryContextCallback + public T searchForObject (Name base, String filter, SearchControls searchControls, ContextMapper mapper) { + List result = search(base, filter, searchControls, mapper); + + if (result.size() == 0) { + throw new EmptyResultDataAccessException(1); + } + else if (result.size() != 1) { + throw new IncorrectResultSizeDataAccessException(1, result.size()); + } + + return result.get(0); + } + + @Override + public T searchForObject(String base, String filter, SearchControls searchControls, ContextMapper mapper) { + return searchForObject(LdapUtils.newLdapName(base), filter, searchControls, mapper); + } + + private static final class NullAuthenticatedLdapEntryContextCallback implements AuthenticatedLdapEntryContextCallback { public void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) { @@ -1563,4 +1577,65 @@ public class LdapTemplate implements LdapOperations, InitializingBean { // Do nothing } } + + @Override + public List search(LdapQuery query, ContextMapper mapper) { + SearchControls searchControls = searchControlsForQuery(query, RETURN_OBJ_FLAG); + + return search(query.base(), + query.filter().encode(), + searchControls, + mapper); + + } + + private SearchControls searchControlsForQuery(LdapQuery query, boolean returnObjFlag) { + SearchControls searchControls = getDefaultSearchControls( + defaultSearchScope, + returnObjFlag, + query.attributes()); + + if(query.searchScope() != null) { + searchControls.setSearchScope(query.searchScope().getId()); + } + + if(query.countLimit() != null) { + searchControls.setCountLimit(query.countLimit()); + } + + if(query.countLimit() != null) { + searchControls.setCountLimit(query.timeLimit()); + } + return searchControls; + } + + @Override + public List search(LdapQuery query, AttributesMapper mapper) { + SearchControls searchControls = searchControlsForQuery(query, DONT_RETURN_OBJ_FLAG); + + return search(query.base(), + query.filter().encode(), + searchControls, + mapper); + } + + @Override + public DirContextOperations searchForContext(LdapQuery query) { + return searchForObject(query, new ContextMapper() { + @Override + public DirContextOperations mapFromContext(Object ctx) throws javax.naming.NamingException { + return (DirContextOperations) ctx; + } + }); + } + + @Override + public T searchForObject(LdapQuery query, ContextMapper mapper) { + SearchControls searchControls = searchControlsForQuery(query, DONT_RETURN_OBJ_FLAG); + + return searchForObject(query.base(), + query.filter().encode(), + searchControls, + mapper); + } } diff --git a/core/src/main/java/org/springframework/ldap/filter/BinaryLogicalFilter.java b/core/src/main/java/org/springframework/ldap/filter/BinaryLogicalFilter.java index f0cf5dd8..348c15fd 100644 --- a/core/src/main/java/org/springframework/ldap/filter/BinaryLogicalFilter.java +++ b/core/src/main/java/org/springframework/ldap/filter/BinaryLogicalFilter.java @@ -15,6 +15,7 @@ */ package org.springframework.ldap.filter; +import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -90,4 +91,9 @@ public abstract class BinaryLogicalFilter extends AbstractFilter { queryList.add(query); return this; } + + public final BinaryLogicalFilter appendAll(Collection subQueries) { + queryList.addAll(subQueries); + return this; + } } diff --git a/core/src/main/java/org/springframework/ldap/query/ConditionCriteria.java b/core/src/main/java/org/springframework/ldap/query/ConditionCriteria.java new file mode 100644 index 00000000..a57caac5 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/ConditionCriteria.java @@ -0,0 +1,113 @@ +/* + * Copyright 2005-2013 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.query; + +/** + * Constructs a conditional LDAP filter based on the attribute specified in the previous builder step. + * + * @author Mattias Hellborg Arthursson + * + * @see LdapQueryBuilder#where(String) + * @see ContainerCriteria#and(String) + * @see ContainerCriteria#or(String) + * @since 2.0 + */ +public interface ConditionCriteria { + + /** + * Appends an {@link org.springframework.ldap.filter.EqualsFilter}. + * + * @param value the value to compare with. + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.EqualsFilter + */ + ContainerCriteria is(String value); + + /** + * Appends an {@link org.springframework.ldap.filter.GreaterThanOrEqualsFilter}. + * + * @param value the value to compare with. + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.GreaterThanOrEqualsFilter + */ + ContainerCriteria gte(String value); + + /** + * Appends a {@link org.springframework.ldap.filter.LessThanOrEqualsFilter}. + * + * @param value the value to compare with. + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.LessThanOrEqualsFilter + */ + ContainerCriteria lte(String value); + + /** + * Appends a {@link org.springframework.ldap.filter.LikeFilter}. + * + * @param value the value to compare with. + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.LikeFilter + */ + ContainerCriteria like(String value); + + /** + * Appends a {@link org.springframework.ldap.filter.WhitespaceWildcardsFilter}. + * + * @param value the value to compare with. + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.WhitespaceWildcardsFilter + */ + ContainerCriteria whitespaceWildcardsLike(String value); + + /** + * Appends a {@link org.springframework.ldap.filter.PresentFilter}. + * + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.PresentFilter + */ + ContainerCriteria isPresent(); + + /** + * Negates the currently constructed operation. In effect this means that the resulting filter will be + * wrapped in a {@link org.springframework.ldap.filter.NotFilter}. + * + * @return an ContainerCriteria instance that can be used to continue append more criteria + * or as the LdapQuery instance to be used as instance to e.g. + * {@link org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper)}. + * + * @see org.springframework.ldap.filter.NotFilter + */ + ConditionCriteria not(); +} diff --git a/core/src/main/java/org/springframework/ldap/query/ContainerCriteria.java b/core/src/main/java/org/springframework/ldap/query/ContainerCriteria.java new file mode 100644 index 00000000..134741d8 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/ContainerCriteria.java @@ -0,0 +1,63 @@ +/* + * Copyright 2005-2013 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.query; + +/** + * Builder for and/or filters. + * + * @author Mattias Hellborg Arthursson + * @since 2.0 + */ +public interface ContainerCriteria extends LdapQuery { + /** + * Append a logical And condition to the currently built filter. + * + * @param attribute Name of the attribute to specify a condition for. + * @return A ConditionCriteria instance for specifying the compare operation. + * @throws IllegalStateException if {@link #or(String)} has previously been called on this instance. + */ + ConditionCriteria and(String attribute); + + /** + * Append a logical Or condition to the currently built filter. + * + * @param attribute Name of the attribute to specify a condition for. + * @return A ConditionCriteria instance for specifying the compare operation. + * @throws IllegalStateException if {@link #and(String)} has previously been called on this instance. + */ + ConditionCriteria or(String attribute); + + /** + * Append an And condition for a nested criterion. Use {@link org.springframework.ldap.query.LdapQueryBuilder#query()} + * to start the nested condition. Any base query information on the nested builder instance will not be considered. + * + * @param nested the nested criterion. + * + * @return A ConditionCriteria instance for specifying the compare operation. + */ + ContainerCriteria and(ContainerCriteria nested); + + /** + * Append an Or condition for a nested criterion. Use {@link org.springframework.ldap.query.LdapQueryBuilder#query()} + * to start the nested condition. Any base query information on the nested builder instance will not be considered. + * + * @param nested the nested criterion. + * + * @return A ConditionCriteria instance for specifying the compare operation. + */ + ContainerCriteria or(ContainerCriteria nested); +} diff --git a/core/src/main/java/org/springframework/ldap/query/CriteriaContainerType.java b/core/src/main/java/org/springframework/ldap/query/CriteriaContainerType.java new file mode 100644 index 00000000..9de933ab --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/CriteriaContainerType.java @@ -0,0 +1,51 @@ +/* + * Copyright 2005-2013 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.query; + +import org.springframework.ldap.filter.AndFilter; +import org.springframework.ldap.filter.BinaryLogicalFilter; +import org.springframework.ldap.filter.OrFilter; + +/** + * @author Mattias Hellborg Arthursson + * @since 2.0 + */ +enum CriteriaContainerType { + AND { + @Override + public BinaryLogicalFilter constructFilter() { + return new AndFilter(); + } + }, OR { + @Override + public BinaryLogicalFilter constructFilter() { + return new OrFilter(); + } + }; + + public void validateSameType(CriteriaContainerType oldType) { + if (oldType != null && oldType != this) { + throw new IllegalStateException( + String.format("Container type has already been specified as %s, cannot change it to %s", + oldType.toString(), + this.toString())); + } + + } + + public abstract BinaryLogicalFilter constructFilter(); +} diff --git a/core/src/main/java/org/springframework/ldap/query/DefaultConditionCriteria.java b/core/src/main/java/org/springframework/ldap/query/DefaultConditionCriteria.java new file mode 100644 index 00000000..e9211671 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/DefaultConditionCriteria.java @@ -0,0 +1,89 @@ +/* + * Copyright 2005-2013 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.query; + +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.Filter; +import org.springframework.ldap.filter.GreaterThanOrEqualsFilter; +import org.springframework.ldap.filter.LessThanOrEqualsFilter; +import org.springframework.ldap.filter.LikeFilter; +import org.springframework.ldap.filter.NotFilter; +import org.springframework.ldap.filter.PresentFilter; +import org.springframework.ldap.filter.WhitespaceWildcardsFilter; + +/** + * @author Mattias Hellborg Arthursson + * @since 2.0 + */ +class DefaultConditionCriteria implements ConditionCriteria { + private final DefaultContainerCriteria parent; + private final String attribute; + private boolean negated = false; + + DefaultConditionCriteria(DefaultContainerCriteria parent, String attribute) { + this.parent = parent; + this.attribute = attribute; + } + + @Override + public ContainerCriteria is(String value) { + return appendToParent(new EqualsFilter(attribute, value)); + } + + @Override + public ContainerCriteria gte(String value) { + return appendToParent(new GreaterThanOrEqualsFilter(attribute, value)); + } + + @Override + public ContainerCriteria lte(String value) { + return appendToParent(new LessThanOrEqualsFilter(attribute, value)); + } + + @Override + public ContainerCriteria like(String value) { + return appendToParent(new LikeFilter(attribute, value)); + } + + @Override + public ContainerCriteria whitespaceWildcardsLike(String value) { + return appendToParent(new WhitespaceWildcardsFilter(attribute, value)); + } + + @Override + public ContainerCriteria isPresent() { + return appendToParent(new PresentFilter(attribute)); + } + + private ContainerCriteria appendToParent(Filter filter) { + return parent.append(negateIfApplicable(filter)); + } + + private Filter negateIfApplicable(Filter myFilter) { + if (negated) { + return new NotFilter(myFilter); + } + + return myFilter; + } + + @Override + public DefaultConditionCriteria not() { + negated = !negated; + return this; + } +} diff --git a/core/src/main/java/org/springframework/ldap/query/DefaultContainerCriteria.java b/core/src/main/java/org/springframework/ldap/query/DefaultContainerCriteria.java new file mode 100644 index 00000000..680a867e --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/DefaultContainerCriteria.java @@ -0,0 +1,129 @@ +/* + * Copyright 2005-2013 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.query; + +import org.springframework.ldap.filter.Filter; + +import javax.naming.Name; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.springframework.ldap.query.CriteriaContainerType.AND; +import static org.springframework.ldap.query.CriteriaContainerType.OR; + +/** + * @author Mattias Hellborg Arthursson + * @since 2.0 + */ +class DefaultContainerCriteria implements ContainerCriteria { + private final Set filters = new LinkedHashSet(); + private final LdapQuery topQuery; + private CriteriaContainerType type; + + DefaultContainerCriteria(LdapQuery topQuery) { + this.topQuery = topQuery; + } + + DefaultContainerCriteria append(Filter filter) { + this.filters.add(filter); + return this; + } + + DefaultContainerCriteria withType(CriteriaContainerType newType) { + this.type = newType; + return this; + } + + @Override + public ConditionCriteria and(String attribute) { + AND.validateSameType(type); + type = AND; + + return new DefaultConditionCriteria(this, attribute); + } + + @Override + public ConditionCriteria or(String attribute) { + OR.validateSameType(type); + type = OR; + + return new DefaultConditionCriteria(this, attribute); + } + + @Override + public ContainerCriteria and(ContainerCriteria nested) { + if(type == OR) { + return new DefaultContainerCriteria(topQuery) + .withType(AND) + .append(this.filter()) + .append(nested.filter()); + } else { + type = AND; + this.filters.add(nested.filter()); + return this; + } + } + + @Override + public ContainerCriteria or(ContainerCriteria nested) { + if (type == AND) { + return new DefaultContainerCriteria(topQuery) + .withType(OR) + .append(this.filter()) + .append(nested.filter()); + } else { + type = OR; + this.filters.add(nested.filter()); + return this; + } + } + + @Override + public Filter filter() { + + if(filters.size() == 1) { + return filters.iterator().next(); + } + + return type.constructFilter().appendAll(filters); + } + + @Override + public Name base() { + return topQuery.base(); + } + + @Override + public SearchScope searchScope() { + return topQuery.searchScope(); + } + + @Override + public Integer timeLimit() { + return topQuery.timeLimit(); + } + + @Override + public Integer countLimit() { + return topQuery.countLimit(); + } + + @Override + public String[] attributes() { + return topQuery.attributes(); + } +} diff --git a/core/src/main/java/org/springframework/ldap/query/LdapQuery.java b/core/src/main/java/org/springframework/ldap/query/LdapQuery.java new file mode 100644 index 00000000..0e783e64 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/LdapQuery.java @@ -0,0 +1,77 @@ +/* + * Copyright 2005-2013 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.query; + +import org.springframework.ldap.filter.Filter; + +import javax.naming.Name; + +/** + * Holds all information regarding a Ldap query to be performed. Contains information regarding search base, + * search scope, time and count limits, and search filter. + * + * @author Mattias Hellborg Arthursson + * @since 2.0 + * @see LdapQueryBuilder + * + * @see org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.AttributesMapper) + * @see org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper) + * @see org.springframework.ldap.core.LdapOperations#searchForObject(LdapQuery, org.springframework.ldap.core.ContextMapper) + * @see org.springframework.ldap.core.LdapOperations#searchForContext(LdapQuery) + */ +public interface LdapQuery { + /** + * Get the search base. Default is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}. + * + * @return the search base. + */ + Name base(); + + /** + * Get the search scope. Default is null, indicating that the LdapTemplate default should be used. + * + * @return the search scope. + */ + SearchScope searchScope(); + + /** + * Get the time limit. Default is null, indicating that the LdapTemplate default should be used. + * + * @return the time limit. + */ + Integer timeLimit(); + + /** + * Get the count limit. Default is null, indicating that the LdapTemplate default should be used. + * @return the count limit. + */ + Integer countLimit(); + + /** + * Get the attributes to return. Default is null, indicating that all attributes should be returned. + * + * @return the attributes to return. + */ + String[] attributes(); + + /** + * Get the filter. + * + * @return the filter. + */ + Filter filter(); +} diff --git a/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java b/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java new file mode 100644 index 00000000..b74cc9ed --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java @@ -0,0 +1,199 @@ +/* + * Copyright 2005-2013 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.query; + +import org.springframework.ldap.filter.Filter; +import org.springframework.ldap.support.LdapUtils; + +import javax.naming.Name; + +/** + * Builder of LdapQueries. Start with a call to {@link #query()}, proceed with specifying the + * basic search configuration (e.g. search base, time limit, etc.), finally specify the actual query. + * Example: + *
+ * import static org.springframework.ldap.query.LdapQueryBuilder.query;
+ * ...
+ *
+ * LdapQuery query = query()
+ *  .base("dc=261consulting, dc=com")
+ *  .searchScope(SearchScope.ONELEVEL)
+ *  .timeLimit(200)
+ *  .countLimit(221)
+ *  .where("objectclass").is("person").and("cn").is("John Doe");
+ * 
+ *

+ * Default configuration is that base path is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}. + * All other parameters are undefined, meaning that (in the case of base search parameters), the LdapTemplate + * defaults will be used. Filter conditions must always be specified. + *

+ * @author Mattias Hellborg Arthursson + * + * @see javax.naming.directory.SearchControls + * @see org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.AttributesMapper) + * @see org.springframework.ldap.core.LdapOperations#search(LdapQuery, org.springframework.ldap.core.ContextMapper) + * @see org.springframework.ldap.core.LdapOperations#searchForObject(LdapQuery, org.springframework.ldap.core.ContextMapper) + * @see org.springframework.ldap.core.LdapOperations#searchForContext(LdapQuery) + */ +public class LdapQueryBuilder implements LdapQuery { + private Name base = LdapUtils.emptyLdapName(); + private SearchScope searchScope = null; + private Integer countLimit = null; + private Integer timeLimit = null; + private String[] attributes = null; + + private DefaultContainerCriteria rootContainer = null; + + /** + * Not to be instantiated directly - use static query() method. + */ + private LdapQueryBuilder() { + + } + + /** + * Construct a new LdapQueryBuilder. + * + * @return a new instance. + */ + public static LdapQueryBuilder query() { + return new LdapQueryBuilder(); + } + + /** + * Set the base search path for the query. + * Default is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}. + * + * @param baseDn the base search path. + * @return this instance. + */ + public LdapQueryBuilder base(String baseDn) { + assertFilterNotStarted(); + this.base = LdapUtils.newLdapName(baseDn); + return this; + } + + /** + * Set the base search path for the query. + * Default is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}. + * + * @param baseDn the base search path. + * @return this instance. + */ + public LdapQueryBuilder base(Name baseDn) { + assertFilterNotStarted(); + this.base = LdapUtils.newLdapName(baseDn); + return this; + } + + /** + * Set the search scope for the query. + * Default is {@link SearchScope#SUBTREE}. + * + * @param searchScope the search scope. + * @return this instance. + */ + public LdapQueryBuilder searchScope(SearchScope searchScope) { + assertFilterNotStarted(); + this.searchScope = searchScope; + return this; + } + + /** + * Set the count limit for the query. + * Default is 0 (no limit). + * + * @param countLimit the count limit. + * @return this instance. + */ + public LdapQueryBuilder countLimit(int countLimit) { + assertFilterNotStarted(); + this.countLimit = countLimit; + return this; + } + + public LdapQueryBuilder attributes(String... attributesToReturn) { + assertFilterNotStarted(); + this.attributes = attributesToReturn; + return this; + } + + /** + * Set the time limit for the query. + * Default is 0 (no limit). + * + * @param timeLimit the time limit. + * @return this instance. + */ + public LdapQueryBuilder timeLimit(int timeLimit) { + assertFilterNotStarted(); + this.timeLimit = timeLimit; + return this; + } + + /** + * Start specifying the filter conditions in this query. + * + * @param attribute The attribute that the first part of the filter should test against. + * @return A ConditionCriteria instance for specifying the compare operation. + */ + public ConditionCriteria where(String attribute) { + assertFilterNotStarted(); + rootContainer = new DefaultContainerCriteria(this); + return new DefaultConditionCriteria(rootContainer, attribute); + } + + private void assertFilterNotStarted() { + if(rootContainer != null) { + throw new IllegalStateException("Invalid operation - filter condition specification already started"); + } + } + + @Override + public Name base() { + return base; + } + + @Override + public SearchScope searchScope() { + return searchScope; + } + + @Override + public Integer countLimit() { + return countLimit; + } + + @Override + public Integer timeLimit() { + return timeLimit; + } + + @Override + public String[] attributes() { + return attributes; + } + + + @Override + public Filter filter() { + if(rootContainer == null) { + throw new IllegalStateException("No filter conditions have been specified specified"); + } + return rootContainer.filter(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/springframework/ldap/query/SearchScope.java b/core/src/main/java/org/springframework/ldap/query/SearchScope.java new file mode 100644 index 00000000..e18db50d --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/query/SearchScope.java @@ -0,0 +1,49 @@ +/* + * Copyright 2005-2013 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.query; + +import javax.naming.directory.SearchControls; + +/** + * Type safe definitions of search scopes. + * + * @author Mattias Hellborg Arthursson + */ +public enum SearchScope { + /** + * Corresponds to {@link SearchControls#OBJECT_SCOPE} + */ + OBJECT(SearchControls.OBJECT_SCOPE), + /** + * Corresponds to {@link SearchControls#ONELEVEL_SCOPE} + */ + ONELEVEL(SearchControls.ONELEVEL_SCOPE), + /** + * Corresponds to {@link SearchControls#SUBTREE_SCOPE} + */ + SUBTREE(SearchControls.SUBTREE_SCOPE); + + private final int id; + + private SearchScope(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java b/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java new file mode 100644 index 00000000..23a217d8 --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java @@ -0,0 +1,129 @@ +package org.springframework.ldap.query; + +import org.junit.Test; +import org.springframework.ldap.support.LdapUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.springframework.ldap.query.LdapQueryBuilder.query; + +/** + * @author Mattias Hellborg Arthursson + */ +public class LdapQueryBuilderTest { + + @Test + public void buildSimpleWithDefaults() { + LdapQuery result = query().where("cn").is("John Doe"); + + assertEquals(LdapUtils.emptyLdapName(), result.base()); + assertNull(result.searchScope()); + assertNull(result.timeLimit()); + assertNull(result.countLimit()); + assertEquals("(cn=John Doe)", result.filter().encode()); + } + + @Test + public void buildGreaterThanOrEquals() { + LdapQuery result = query().where("cn").gte("John Doe"); + + assertEquals("(cn>=John Doe)", result.filter().encode()); + } + + @Test + public void buildLessThanOrEquals() { + LdapQuery result = query().where("cn").lte("John Doe"); + + assertEquals("(cn<=John Doe)", result.filter().encode()); + } + + @Test + public void buildLike() { + LdapQuery result = query().where("cn").like("J*hn Doe"); + + assertEquals("(cn=J*hn Doe)", result.filter().encode()); + } + + + @Test + public void buildWhitespaceWildcards() { + LdapQuery result = query().where("cn").whitespaceWildcardsLike("John Doe"); + + assertEquals("(cn=*John*Doe*)", result.filter().encode()); + } + + @Test + public void buildPresent() { + LdapQuery result = query().where("cn").isPresent(); + + assertEquals("(cn=*)", result.filter().encode()); + } + + @Test + public void testBuildSimpleAnd() { + LdapQuery query = query() + .base("dc=261consulting, dc=com") + .searchScope(SearchScope.ONELEVEL) + .timeLimit(200) + .countLimit(221) + .where("objectclass").is("person").and("cn").is("John Doe"); + + assertEquals(LdapUtils.newLdapName("dc=261consulting, dc=com"), query.base()); + assertEquals(SearchScope.ONELEVEL, query.searchScope()); + assertEquals(Integer.valueOf(200), query.timeLimit()); + assertEquals(Integer.valueOf(221), query.countLimit()); + assertEquals("(&(objectclass=person)(cn=John Doe))", query.filter().encode()); + } + + @Test + public void buildSimpleOr() { + LdapQuery result = query().where("objectclass").is("person").or("cn").is("John Doe"); + + assertEquals("(|(objectclass=person)(cn=John Doe))", result.filter().encode()); + } + + @Test + public void buildAndOrPrecedence() { + LdapQuery result = query().where("objectclass").is("person") + .and("cn").is("John Doe") + .or(query().where("sn").is("Doe")); + + assertEquals("(|(&(objectclass=person)(cn=John Doe))(sn=Doe))", result.filter().encode()); + } + + @Test + public void buildOrNegatedSubQueries() { + LdapQuery result = query().where("objectclass").not().is("person").or("sn").not().is("Doe"); + assertEquals("(|(!(objectclass=person))(!(sn=Doe)))", result.filter().encode()); + } + + @Test + public void buildNestedAnd() { + LdapQuery result = query() + .where("objectclass").is("person") + .and(query() + .where("sn").is("Doe") + .or("sn").like("Die")); + assertEquals("(&(objectclass=person)(|(sn=Doe)(sn=Die)))", result.filter().encode()); + } + + @Test(expected = IllegalStateException.class) + public void verifyEmptyFilterThrowsIllegalState() { + query().filter(); + } + + @Test(expected = IllegalStateException.class) + public void verifyThatNewAttemptToStartSpecifyingFilterThrowsIllegalState() { + LdapQueryBuilder query = query(); + query.where("sn").is("Doe"); + query.where("cn").is("John Doe"); + } + + @Test(expected = IllegalStateException.class) + public void verifyThatAttemptToStartSpecifyingBasePropertiesThrowsIllegalStateWhenFilterStarted() { + LdapQueryBuilder query = query(); + query.where("sn").is("Doe"); + query.base("dc=261consulting,dc=com"); + } + +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/LdapTemplateSearchResultITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/LdapTemplateSearchResultITest.java index 0b565234..989831dd 100644 --- a/test/integration-tests/src/test/java/org/springframework/ldap/itest/LdapTemplateSearchResultITest.java +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/LdapTemplateSearchResultITest.java @@ -26,6 +26,7 @@ import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.AbstractContextMapper; +import org.springframework.ldap.query.SearchScope; import org.springframework.ldap.support.LdapUtils; import org.springframework.ldap.test.AttributeCheckAttributesMapper; import org.springframework.ldap.test.AttributeCheckContextMapper; @@ -39,6 +40,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; +import static org.springframework.ldap.query.LdapQueryBuilder.query; /** * Tests for LdapTemplate's search methods. This test class tests all the @@ -86,7 +88,7 @@ public class LdapTemplateSearchResultITest extends AbstractLdapTemplateIntegrati contextMapper = null; } - @Test + @Test public void testSearch_AttributesMapper() { attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); attributesMapper.setExpectedValues(ALL_VALUES); @@ -94,7 +96,81 @@ public class LdapTemplateSearchResultITest extends AbstractLdapTemplateIntegrati assertEquals(1, list.size()); } - @Test + @Test + public void testSearch_LdapQuery_AttributesMapper() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search(query() + .base(BASE_STRING) + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_FewerAttributes() { + attributesMapper.setExpectedAttributes(new String[] {"cn"}); + attributesMapper.setExpectedValues(new String[]{"Some Person2"}); + + List list = tested.search(query() + .base(BASE_STRING) + .attributes("cn") + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_SearchScope() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search(query() + .base(BASE_STRING) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(0, list.size()); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_SearchScope_CorrectBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search(query() + .base("ou=company1,c=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_NoBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search(query() + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_DifferentBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search(query() + .base("c=Norway") + .where("objectclass").is("person").and("sn").is("Person2"), + attributesMapper); + assertEquals(0, list.size()); + } + + @Test public void testSearch_SearchScope_AttributesMapper() { attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); attributesMapper.setExpectedValues(ALL_VALUES); @@ -200,7 +276,80 @@ public class LdapTemplateSearchResultITest extends AbstractLdapTemplateIntegrati assertEquals(1, list.size()); } - @Test + @Test + public void testSearch_ContextMapper_LdapQuery() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search(query() + .base(BASE_NAME) + .where("objectclass").is("person").and("sn").is("Person2"), + contextMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_NoBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search(query() + .where("objectclass").is("person").and("sn").is("Person2"), + contextMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_SearchScope() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search(query() + .base(BASE_NAME) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2"), + contextMapper); + assertEquals(0, list.size()); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_SearchScope_CorrectBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search(query() + .base("ou=company1,c=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2"), + contextMapper); + assertEquals(1, list.size()); + } + + @Test + public void testSearchForContext_LdapQuery() { + DirContextOperations result = tested.searchForContext(query() + .where("objectclass").is("person").and("sn").is("Person2")); + + assertNotNull(result); + assertEquals("Person2", result.getStringAttribute("sn")); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void testSearchForContext_LdapQuery_SearchScopeNotFound() { + tested.searchForContext(query() + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")); + } + + @Test + public void testSearchForContext_LdapQuery_SearchScope_CorrectBase() { + DirContextOperations result = + tested.searchForContext(query() + .searchScope(SearchScope.ONELEVEL) + .base("ou=company1,c=Sweden") + .where("objectclass").is("person").and("sn").is("Person2")); + + assertNotNull(result); + assertEquals("Person2", result.getStringAttribute("sn")); + } + + @Test public void testSearch_SearchScope_ContextMapper_Name() { contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); contextMapper.setExpectedValues(ALL_VALUES);