From de3b8ffc5f4ba664330d7681ebae17078882d290 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 23 Oct 2024 19:36:57 +0200 Subject: [PATCH] Add `@LdapEncode` annotation to configure encoding of LDAP parameters. Closes #509 Original pull request: #518 --- .../data/ldap/repository/LdapEncode.java | 53 ++++++++++++ .../data/ldap/repository/LdapEncoder.java | 32 +++++++ .../ldap/repository/query/LdapParameters.java | 84 +++++++++++++++++++ .../repository/query/LdapQueryMethod.java | 11 ++- .../repository/query/StringBasedQuery.java | 54 ++++++++++-- ...AnnotatedLdapRepositoryQueryUnitTests.java | 29 ++++++- 6 files changed, 252 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/springframework/data/ldap/repository/LdapEncode.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java create mode 100644 src/main/java/org/springframework/data/ldap/repository/query/LdapParameters.java diff --git a/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java b/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java new file mode 100644 index 0000000..b4a5837 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java @@ -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 value(); + + /** + * {@link LdapEncoder} to instantiate to encode query parameters. + * + * @return {@link LdapEncoder} class + */ + @AliasFor("value") + Class encoder() default LdapEncoder.class; + +} diff --git a/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java b/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java new file mode 100644 index 0000000..6e49197 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java @@ -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); +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapParameters.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapParameters.java new file mode 100644 index 0000000..c5cedc6 --- /dev/null +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapParameters.java @@ -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 { + + 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 parameters, TypeInformation domainType) { + + super(parameters); + this.domainType = domainType; + } + + @Override + protected LdapParameters createFrom(List 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; + } + } + +} diff --git a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java index e405e9c..16313d3 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java @@ -21,7 +21,6 @@ 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 LdapQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFac 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}. * diff --git a/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java index 28872c4..9fb5f1e 100644 --- a/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java +++ b/src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java @@ -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.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.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 @@ class StringBasedQuery { private final String query; - private final Parameters parameters; + private final LdapParameters parameters; private final List 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 @@ public static String bind(String input, List parameters) { */ static class BindingContext { - private final Parameters parameters; + private final LdapParameters parameters; private final ParameterAccessor parameterAccessor; private final List bindings; private final Function evaluator; @@ -306,7 +308,7 @@ static class BindingContext { /** * Create new {@link BindingContext}. */ - BindingContext(Parameters parameters, ParameterAccessor parameterAccessor, List bindings, + BindingContext(LdapParameters parameters, ParameterAccessor parameterAccessor, List bindings, Function evaluator) { this.parameters = parameters; @@ -356,11 +358,15 @@ private Object getParameterValueForBinding(ParameterBinding binding) { 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 @@ static ParameterBinding named(String name) { 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 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); } diff --git a/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryUnitTests.java b/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryUnitTests.java index a680ef8..3c1e9d9 100644 --- a/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryUnitTests.java +++ b/src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryUnitTests.java @@ -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.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 @@ void shouldEncodeBase() throws NoSuchMethodException { 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 @@ interface QueryRepository extends LdapRepository { @Query(base = ":dc", value = "(cn=:fullName)") List baseNamedParameters(String fullName, String dc); + @Query(value = "(cn=:fullName)") + List customEncoder(@LdapEncode(MyEncoder.class) String fullName); + + } + + static class MyEncoder implements LdapEncoder { + + @Override + public String filterEncode(String value) { + return value + "bar"; + } } }