Add @LdapEncode annotation to configure encoding of LDAP parameters.

Closes #509
Original pull request: #518
This commit is contained in:
Marcin Grzejszczak
2024-10-23 19:36:57 +02:00
committed by Mark Paluch
parent c9a45fb33f
commit de3b8ffc5f
6 changed files with 252 additions and 11 deletions

View File

@@ -0,0 +1,53 @@
/*
* 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Allows passing of custom {@link LdapEncoder}.
*
* @author Marcin Grzejszczak
* @since 3.5.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LdapEncode {
/**
* {@link LdapEncoder} to instantiate to encode query parameters.
*
* @return {@link LdapEncoder} class
*/
@AliasFor("encoder")
Class<? extends LdapEncoder> value();
/**
* {@link LdapEncoder} to instantiate to encode query parameters.
*
* @return {@link LdapEncoder} class
*/
@AliasFor("value")
Class<? extends LdapEncoder> encoder() default LdapEncoder.class;
}

View File

@@ -0,0 +1,32 @@
/*
* 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;
/**
* Allows plugging in custom encoding for {@link LdapEncode}.
*
* @author Marcin Grzejszczak
* @since 3.5.0
*/
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.
*/
String filterEncode(String value);
}

View File

@@ -0,0 +1,84 @@
/*
* 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.query;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.geo.Distance;
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;
/**
* Custom extension of {@link Parameters} discovering additional
*
* @author Marcin Grzejszczak
* @since 3.5.0
*/
public class LdapParameters extends Parameters<LdapParameters, LdapParameters.LdapParameter> {
private final TypeInformation<?> domainType;
/**
* Creates a new {@link LdapParameters} instance from the given {@link Method} and {@link LdapQueryMethod}.
*
* @param parametersSource must not be {@literal null}.
*/
public LdapParameters(ParametersSource parametersSource) {
super(parametersSource, methodParameter -> new LdapParameter(methodParameter,
parametersSource.getDomainTypeInformation()));
this.domainType = parametersSource.getDomainTypeInformation();
}
private LdapParameters(List<LdapParameter> parameters, TypeInformation<?> domainType) {
super(parameters);
this.domainType = domainType;
}
@Override
protected LdapParameters createFrom(List<LdapParameter> parameters) {
return new LdapParameters(parameters, this.domainType);
}
/**
* Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones.
*
* @author Marcin Grzejszczak
*/
static class LdapParameter extends Parameter {
final MethodParameter parameter;
/**
* Creates a new {@link LdapParameter}.
*
* @param parameter must not be {@literal null}.
* @param domainType must not be {@literal null}.
*/
LdapParameter(MethodParameter parameter, TypeInformation<?> domainType) {
super(parameter, domainType);
this.parameter = parameter;
}
}
}

View File

@@ -21,7 +21,6 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.lang.Nullable;
@@ -50,6 +49,16 @@ public class LdapQueryMethod extends QueryMethod {
this.method = method;
}
@Override
protected LdapParameters createParameters(ParametersSource parametersSource) {
return new LdapParameters(parametersSource);
}
@Override
public LdapParameters getParameters() {
return (LdapParameters) super.getParameters();
}
/**
* Check whether the target method is annotated with {@link org.springframework.data.ldap.repository.Query}.
*

View File

@@ -15,8 +15,6 @@
*/
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;
@@ -27,8 +25,10 @@ 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,6 +39,8 @@ 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.
*
@@ -48,7 +50,7 @@ import org.springframework.util.StringUtils;
class StringBasedQuery {
private final String query;
private final Parameters<?, ?> parameters;
private final LdapParameters parameters;
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
private final ExpressionDependencies expressionDependencies;
@@ -59,7 +61,7 @@ class StringBasedQuery {
* @param parameters must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
*/
public StringBasedQuery(String query, Parameters<?, ?> parameters, ValueExpressionDelegate expressionParser) {
public StringBasedQuery(String query, LdapParameters parameters, ValueExpressionDelegate expressionParser) {
this.query = ParameterBindingParser.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings, expressionParser);
@@ -298,7 +300,7 @@ class StringBasedQuery {
*/
static class BindingContext {
private final Parameters<?, ?> parameters;
private final LdapParameters parameters;
private final ParameterAccessor parameterAccessor;
private final List<ParameterBinding> bindings;
private final Function<ValueExpression, Object> evaluator;
@@ -306,7 +308,7 @@ class StringBasedQuery {
/**
* Create new {@link BindingContext}.
*/
BindingContext(Parameters<?, ?> parameters, ParameterAccessor parameterAccessor, List<ParameterBinding> bindings,
BindingContext(LdapParameters parameters, ParameterAccessor parameterAccessor, List<ParameterBinding> bindings,
Function<ValueExpression, Object> evaluator) {
this.parameters = parameters;
@@ -356,11 +358,15 @@ class StringBasedQuery {
if (binding.isExpression()) {
return evaluator.apply(binding.getRequiredExpression());
}
Object value = binding.isNamed()
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
: parameterAccessor.getBindableValue(binding.getParameterIndex());
return value == null ? null : LdapEncoder.filterEncode(value.toString());
if (value == null) {
return null;
}
return binding.getEncodedValue(parameters, value);
}
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
@@ -407,6 +413,38 @@ 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);
}

View File

@@ -15,14 +15,14 @@
*/
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.LdapRepository;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -32,6 +32,8 @@ 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}
*
@@ -79,6 +81,18 @@ class AnnotatedLdapRepositoryQueryUnitTests {
assertThat(ldapQuery.base()).hasToString("cn=John\\29");
}
@Test
void shouldEncodeWithCustomEncoder() throws NoSuchMethodException {
LdapQueryMethod method = queryMethod("customEncoder", String.class);
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
LdapQuery ldapQuery = query.createQuery(
new LdapParametersParameterAccessor(method, new Object[] { "Doe" }));
assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=Doebar)");
}
private LdapQueryMethod queryMethod(String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
return new LdapQueryMethod(QueryRepository.class.getMethod(methodName, parameterTypes),
new DefaultRepositoryMetadata(QueryRepository.class), new SpelAwareProxyProjectionFactory());
@@ -100,5 +114,16 @@ class AnnotatedLdapRepositoryQueryUnitTests {
@Query(base = ":dc", value = "(cn=:fullName)")
List<SchemaEntry> baseNamedParameters(String fullName, String dc);
@Query(value = "(cn=:fullName)")
List<SchemaEntry> customEncoder(@LdapEncode(MyEncoder.class) String fullName);
}
static class MyEncoder implements LdapEncoder {
@Override
public String filterEncode(String value) {
return value + "bar";
}
}
}