Polishing.
Refine Javadoc. Move LdapEncoder lookup into LdapParameters. Eagerly instantiate LdapEncoder. Refine method naming. Add NameEncoder and LikeEncoder for simplified usage. See #509 Original pull request: #518
This commit is contained in:
@@ -24,18 +24,34 @@ import java.lang.annotation.Target;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Allows passing of custom {@link LdapEncoder}.
|
||||
* Annotation which indicates that a method parameter should be encoded using a specific {@link LdapEncoder} for a
|
||||
* repository query method invocation.
|
||||
* <p>
|
||||
* If no {@link LdapEncoder} is configured, bound method parameters are encoded using
|
||||
* {@link org.springframework.ldap.support.LdapEncoder#filterEncode(String)}. The default encoder considers chars such
|
||||
* as {@code *} (asterisk) to be encoded which might interfere with the intent of running a Like query. Since Spring
|
||||
* Data LDAP doesn't parse queries it is up to you to decide which encoder to use.
|
||||
* <p>
|
||||
* {@link LdapEncoder} implementations must declare a no-args constructor so they can be instantiated during repository
|
||||
* initialization.
|
||||
* <p>
|
||||
* Note that parameter encoding applies only to parameters that are directly bound to a query. Parameters used in Value
|
||||
* Expressions (SpEL, Configuration Properties) are not considered for encoding and must be encoded properly by using
|
||||
* SpEL Method invocations or a SpEL Extension.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.5.0
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
* @see LdapEncoder.LikeEncoder
|
||||
* @see LdapEncoder.NameEncoder
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LdapEncode {
|
||||
|
||||
/**
|
||||
* {@link LdapEncoder} to instantiate to encode query parameters.
|
||||
* {@link LdapEncoder} to encode query parameters.
|
||||
*
|
||||
* @return {@link LdapEncoder} class
|
||||
*/
|
||||
@@ -43,7 +59,7 @@ public @interface LdapEncode {
|
||||
Class<? extends LdapEncoder> value();
|
||||
|
||||
/**
|
||||
* {@link LdapEncoder} to instantiate to encode query parameters.
|
||||
* {@link LdapEncoder} to encode query parameters.
|
||||
*
|
||||
* @return {@link LdapEncoder} class
|
||||
*/
|
||||
|
||||
@@ -15,18 +15,70 @@
|
||||
*/
|
||||
package org.springframework.data.ldap.repository;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Allows plugging in custom encoding for {@link LdapEncode}.
|
||||
* Strategy interface to escape values for use in LDAP filters.
|
||||
* <p>
|
||||
* Accepts an LDAP filter value to be encoded (escaped) for String-based LDAP query usage as LDAP queries do not feature
|
||||
* an out-of-band parameter binding mechanism.
|
||||
* <p>
|
||||
* Make sure that your implementation escapes special characters in the value adequately to prevent injection attacks.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.5.0
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
*/
|
||||
public interface LdapEncoder {
|
||||
|
||||
/**
|
||||
* Escape a value for use in a filter.
|
||||
* @param value the value to escape.
|
||||
* @return a properly escaped representation of the supplied value.
|
||||
* Encode a value for use in a filter.
|
||||
*
|
||||
* @param value the value to encode.
|
||||
* @return a properly encoded representation of the supplied value.
|
||||
*/
|
||||
String filterEncode(String value);
|
||||
String encode(String value);
|
||||
|
||||
/**
|
||||
* {@link LdapEncoder} using {@link org.springframework.ldap.support.LdapEncoder#nameEncode(String)}. Encodes a value
|
||||
* for use with a DN. Escapes for LDAP, not JNDI!
|
||||
*/
|
||||
class NameEncoder implements LdapEncoder {
|
||||
|
||||
@Override
|
||||
public String encode(String value) {
|
||||
return org.springframework.ldap.support.LdapEncoder.nameEncode(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for use in a filter retaining asterisks ({@code *}) for like/contains searches.
|
||||
*/
|
||||
class LikeEncoder implements LdapEncoder {
|
||||
|
||||
@Override
|
||||
public String encode(String value) {
|
||||
|
||||
if (ObjectUtils.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
String[] substrings = value.split("\\*", -2);
|
||||
|
||||
if (substrings.length == 1) {
|
||||
return org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[0]);
|
||||
}
|
||||
|
||||
StringBuilder buff = new StringBuilder();
|
||||
for (int i = 0; i < substrings.length; i++) {
|
||||
buff.append(org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[i]));
|
||||
if (i < substrings.length - 1) {
|
||||
buff.append("*");
|
||||
}
|
||||
}
|
||||
|
||||
return buff.toString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ import org.springframework.ldap.query.SearchScope;
|
||||
/**
|
||||
* Annotation for use in {@link org.springframework.data.ldap.repository.LdapRepository} declarations to create
|
||||
* automatic query methods based on statically defined queries.
|
||||
* <p>
|
||||
* LDAP queries support parameter bindings (by-index using ?0, by-name using :someParam), SpEL expressions, and
|
||||
* Configuration Properties to create dynamic queries. All non-String arguments are converted to their
|
||||
* {@link Object#toString() toString} representation to render the query.
|
||||
*
|
||||
* @author Mattias Hellborg Arthursson
|
||||
*/
|
||||
@@ -36,7 +40,7 @@ public @interface Query {
|
||||
|
||||
/**
|
||||
* Search base, to be used as input to {@link org.springframework.ldap.query.LdapQueryBuilder#base(javax.naming.Name)}
|
||||
* .
|
||||
* . Supports parameter binding.
|
||||
*
|
||||
* @return the search base, default is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}
|
||||
*/
|
||||
@@ -44,7 +48,7 @@ public @interface Query {
|
||||
|
||||
/**
|
||||
* The filter format string, to be used as input to
|
||||
* {@link org.springframework.ldap.query.LdapQueryBuilder#filter(String, Object...)}.
|
||||
* {@link org.springframework.ldap.query.LdapQueryBuilder#filter(String, Object...)}. Supports parameter binding.
|
||||
*
|
||||
* @return search filter, must be specified.
|
||||
*/
|
||||
|
||||
@@ -18,18 +18,22 @@ package org.springframework.data.ldap.repository.query;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.geo.Distance;
|
||||
import org.springframework.data.ldap.repository.LdapEncode;
|
||||
import org.springframework.data.ldap.repository.LdapEncoder;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.ParametersSource;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Custom extension of {@link Parameters} discovering additional
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.5.0
|
||||
* @since 3.5
|
||||
*/
|
||||
public class LdapParameters extends Parameters<LdapParameters, LdapParameters.LdapParameter> {
|
||||
|
||||
@@ -65,9 +69,10 @@ public class LdapParameters extends Parameters<LdapParameters, LdapParameters.Ld
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
static class LdapParameter extends Parameter {
|
||||
protected static class LdapParameter extends Parameter {
|
||||
|
||||
final MethodParameter parameter;
|
||||
private final @Nullable LdapEncoder ldapEncoder;
|
||||
private final MethodParameter parameter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link LdapParameter}.
|
||||
@@ -76,8 +81,29 @@ public class LdapParameters extends Parameters<LdapParameters, LdapParameters.Ld
|
||||
* @param domainType must not be {@literal null}.
|
||||
*/
|
||||
LdapParameter(MethodParameter parameter, TypeInformation<?> domainType) {
|
||||
|
||||
super(parameter, domainType);
|
||||
this.parameter = parameter;
|
||||
|
||||
LdapEncode encode = parameter.getParameterAnnotation(LdapEncode.class);
|
||||
|
||||
if (encode != null) {
|
||||
this.ldapEncoder = BeanUtils.instantiateClass(encode.value());
|
||||
} else {
|
||||
this.ldapEncoder = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasLdapEncoder() {
|
||||
return ldapEncoder != null;
|
||||
}
|
||||
|
||||
public LdapEncoder getLdapEncoder() {
|
||||
|
||||
if (ldapEncoder == null) {
|
||||
throw new IllegalStateException("No LdapEncoder found for parameter " + parameter);
|
||||
}
|
||||
return ldapEncoder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class LdapQueryCreator extends AbstractQueryCreator<LdapQuery, ContainerCriteria
|
||||
* @param tree must not be {@literal null}.
|
||||
* @param entityType must not be {@literal null}.
|
||||
* @param mapper must not be {@literal null}.
|
||||
* @param values must not be {@literal null}.
|
||||
* @param parameterAccessor must not be {@literal null}.
|
||||
* @param inputProperties must not be {@literal null}.
|
||||
*/
|
||||
LdapQueryCreator(PartTree tree, Class<?> entityType, ObjectDirectoryMapper mapper,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2024 the original author or authors.
|
||||
* Copyright 2024 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.
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.ldap.repository.query;
|
||||
|
||||
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -25,10 +27,8 @@ import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.expression.ValueExpression;
|
||||
import org.springframework.data.expression.ValueExpressionParser;
|
||||
import org.springframework.data.ldap.repository.LdapEncode;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
@@ -39,12 +39,11 @@ import org.springframework.ldap.support.LdapEncoder;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.ParameterBinding;
|
||||
|
||||
/**
|
||||
* String-based Query abstracting a query with parameter bindings.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Mark Paluch
|
||||
* @since 3.5
|
||||
*/
|
||||
class StringBasedQuery {
|
||||
@@ -79,8 +78,7 @@ class StringBasedQuery {
|
||||
|
||||
for (ParameterBinding binding : queryParameterBindings) {
|
||||
if (binding.isExpression()) {
|
||||
dependencies
|
||||
.add(binding.getRequiredExpression().getExpressionDependencies());
|
||||
dependencies.add(binding.getRequiredExpression().getExpressionDependencies());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +254,6 @@ class StringBasedQuery {
|
||||
*/
|
||||
static class ParameterBinder {
|
||||
|
||||
|
||||
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
|
||||
|
||||
@@ -358,15 +355,20 @@ class StringBasedQuery {
|
||||
if (binding.isExpression()) {
|
||||
return evaluator.apply(binding.getRequiredExpression());
|
||||
}
|
||||
Object value = binding.isNamed()
|
||||
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
|
||||
: parameterAccessor.getBindableValue(binding.getParameterIndex());
|
||||
|
||||
int index = binding.isNamed() ? getParameterIndex(parameters, binding.getRequiredParameterName())
|
||||
: binding.getParameterIndex();
|
||||
Object value = parameterAccessor.getBindableValue(index);
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return binding.getEncodedValue(parameters, value);
|
||||
String toString = value.toString();
|
||||
LdapParameters.LdapParameter parameter = parameters.getBindableParameter(index);
|
||||
|
||||
return parameter.hasLdapEncoder() ? parameter.getLdapEncoder().encode(toString)
|
||||
: LdapEncoder.filterEncode(toString);
|
||||
}
|
||||
|
||||
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
|
||||
@@ -413,38 +415,6 @@ class StringBasedQuery {
|
||||
return new ParameterBinding(-1, null, name);
|
||||
}
|
||||
|
||||
Object getEncodedValue(LdapParameters ldapParameters, Object value) {
|
||||
org.springframework.data.ldap.repository.LdapEncoder encoder = encoderForParameter(ldapParameters);
|
||||
if (encoder == null) {
|
||||
return LdapEncoder.filterEncode(value.toString());
|
||||
}
|
||||
return encoder.filterEncode(value.toString());
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
org.springframework.data.ldap.repository.LdapEncoder encoderForParameter(LdapParameters ldapParameters) {
|
||||
for (LdapParameters.LdapParameter parameter : ldapParameters) {
|
||||
if (isByName(parameter) || isByIndex(parameter)) {
|
||||
LdapEncode ldapEncode = parameter.parameter.getParameterAnnotation(LdapEncode.class);
|
||||
if (ldapEncode == null) {
|
||||
return null;
|
||||
}
|
||||
Class<? extends org.springframework.data.ldap.repository.LdapEncoder> encoder = ldapEncode.value();
|
||||
return BeanUtils.instantiateClass(encoder);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isByIndex(LdapParameters.LdapParameter parameter) {
|
||||
return parameterIndex != -1 && parameter.getIndex() == parameterIndex;
|
||||
}
|
||||
|
||||
private boolean isByName(LdapParameters.LdapParameter parameter) {
|
||||
return parameterName != null && parameterName.equals(parameter.getName().orElse(null));
|
||||
}
|
||||
|
||||
boolean isNamed() {
|
||||
return (parameterName != null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2024 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
|
||||
*
|
||||
* https://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.data.ldap.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link LdapEncoder}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class LdapEncoderUnitTests {
|
||||
|
||||
@Test // GH-509
|
||||
void shouldEncodeName() {
|
||||
|
||||
String result = new LdapEncoder.NameEncoder().encode("# foo ,+\"\\<>; ");
|
||||
|
||||
assertThat(result).isEqualTo("\\# foo \\,\\+\\\"\\\\\\<\\>\\;\\ ");
|
||||
}
|
||||
|
||||
@Test // GH-509
|
||||
void shouldEncodeLikeFilter() {
|
||||
|
||||
String result = new LdapEncoder.LikeEncoder().encode("*hugo*ern(o)*");
|
||||
|
||||
assertThat(result).isEqualTo("*hugo*ern\\28o\\29*");
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,16 @@
|
||||
*/
|
||||
package org.springframework.data.ldap.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.data.ldap.core.mapping.LdapMappingContext;
|
||||
import org.springframework.data.ldap.repository.LdapEncoder;
|
||||
import org.springframework.data.ldap.repository.LdapEncode;
|
||||
import org.springframework.data.ldap.repository.LdapEncoder;
|
||||
import org.springframework.data.ldap.repository.LdapRepository;
|
||||
import org.springframework.data.ldap.repository.Query;
|
||||
import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
@@ -32,8 +34,6 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.ldap.core.LdapOperations;
|
||||
import org.springframework.ldap.query.LdapQuery;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AnnotatedLdapRepositoryQuery}
|
||||
*
|
||||
@@ -122,7 +122,7 @@ class AnnotatedLdapRepositoryQueryUnitTests {
|
||||
static class MyEncoder implements LdapEncoder {
|
||||
|
||||
@Override
|
||||
public String filterEncode(String value) {
|
||||
public String encode(String value) {
|
||||
return value + "bar";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user