Skip to content

Commit de3b8ff

Browse files
marcingrzejszczakmp911de
authored andcommitted
Add @LdapEncode annotation to configure encoding of LDAP parameters.
Closes #509 Original pull request: #518
1 parent c9a45fb commit de3b8ff

File tree

6 files changed

+252
-11
lines changed

6 files changed

+252
-11
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.ldap.repository;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.core.annotation.AliasFor;
25+
26+
/**
27+
* Allows passing of custom {@link LdapEncoder}.
28+
*
29+
* @author Marcin Grzejszczak
30+
* @since 3.5.0
31+
*/
32+
@Target(ElementType.PARAMETER)
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Documented
35+
public @interface LdapEncode {
36+
37+
/**
38+
* {@link LdapEncoder} to instantiate to encode query parameters.
39+
*
40+
* @return {@link LdapEncoder} class
41+
*/
42+
@AliasFor("encoder")
43+
Class<? extends LdapEncoder> value();
44+
45+
/**
46+
* {@link LdapEncoder} to instantiate to encode query parameters.
47+
*
48+
* @return {@link LdapEncoder} class
49+
*/
50+
@AliasFor("value")
51+
Class<? extends LdapEncoder> encoder() default LdapEncoder.class;
52+
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.ldap.repository;
17+
18+
/**
19+
* Allows plugging in custom encoding for {@link LdapEncode}.
20+
*
21+
* @author Marcin Grzejszczak
22+
* @since 3.5.0
23+
*/
24+
public interface LdapEncoder {
25+
26+
/**
27+
* Escape a value for use in a filter.
28+
* @param value the value to escape.
29+
* @return a properly escaped representation of the supplied value.
30+
*/
31+
String filterEncode(String value);
32+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.ldap.repository.query;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.List;
20+
21+
import org.springframework.core.MethodParameter;
22+
import org.springframework.data.geo.Distance;
23+
import org.springframework.data.repository.query.Parameter;
24+
import org.springframework.data.repository.query.Parameters;
25+
import org.springframework.data.repository.query.ParametersSource;
26+
import org.springframework.data.util.TypeInformation;
27+
28+
/**
29+
* Custom extension of {@link Parameters} discovering additional
30+
*
31+
* @author Marcin Grzejszczak
32+
* @since 3.5.0
33+
*/
34+
public class LdapParameters extends Parameters<LdapParameters, LdapParameters.LdapParameter> {
35+
36+
private final TypeInformation<?> domainType;
37+
38+
/**
39+
* Creates a new {@link LdapParameters} instance from the given {@link Method} and {@link LdapQueryMethod}.
40+
*
41+
* @param parametersSource must not be {@literal null}.
42+
*/
43+
public LdapParameters(ParametersSource parametersSource) {
44+
45+
super(parametersSource, methodParameter -> new LdapParameter(methodParameter,
46+
parametersSource.getDomainTypeInformation()));
47+
48+
this.domainType = parametersSource.getDomainTypeInformation();
49+
}
50+
51+
private LdapParameters(List<LdapParameter> parameters, TypeInformation<?> domainType) {
52+
53+
super(parameters);
54+
this.domainType = domainType;
55+
}
56+
57+
@Override
58+
protected LdapParameters createFrom(List<LdapParameter> parameters) {
59+
return new LdapParameters(parameters, this.domainType);
60+
}
61+
62+
63+
/**
64+
* Custom {@link Parameter} implementation adding parameters of type {@link Distance} to the special ones.
65+
*
66+
* @author Marcin Grzejszczak
67+
*/
68+
static class LdapParameter extends Parameter {
69+
70+
final MethodParameter parameter;
71+
72+
/**
73+
* Creates a new {@link LdapParameter}.
74+
*
75+
* @param parameter must not be {@literal null}.
76+
* @param domainType must not be {@literal null}.
77+
*/
78+
LdapParameter(MethodParameter parameter, TypeInformation<?> domainType) {
79+
super(parameter, domainType);
80+
this.parameter = parameter;
81+
}
82+
}
83+
84+
}

src/main/java/org/springframework/data/ldap/repository/query/LdapQueryMethod.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.springframework.data.ldap.repository.Query;
2222
import org.springframework.data.projection.ProjectionFactory;
2323
import org.springframework.data.repository.core.RepositoryMetadata;
24-
import org.springframework.data.repository.query.Parameters;
2524
import org.springframework.data.repository.query.ParametersSource;
2625
import org.springframework.data.repository.query.QueryMethod;
2726
import org.springframework.lang.Nullable;
@@ -50,6 +49,16 @@ public LdapQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFac
5049
this.method = method;
5150
}
5251

52+
@Override
53+
protected LdapParameters createParameters(ParametersSource parametersSource) {
54+
return new LdapParameters(parametersSource);
55+
}
56+
57+
@Override
58+
public LdapParameters getParameters() {
59+
return (LdapParameters) super.getParameters();
60+
}
61+
5362
/**
5463
* Check whether the target method is annotated with {@link org.springframework.data.ldap.repository.Query}.
5564
*

src/main/java/org/springframework/data/ldap/repository/query/StringBasedQuery.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
*/
1616
package org.springframework.data.ldap.repository.query;
1717

18-
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.*;
19-
2018
import java.util.ArrayList;
2119
import java.util.Collections;
2220
import java.util.List;
@@ -27,8 +25,10 @@
2725
import java.util.regex.Matcher;
2826
import java.util.regex.Pattern;
2927

28+
import org.springframework.beans.BeanUtils;
3029
import org.springframework.data.expression.ValueExpression;
3130
import org.springframework.data.expression.ValueExpressionParser;
31+
import org.springframework.data.ldap.repository.LdapEncode;
3232
import org.springframework.data.repository.query.Parameter;
3333
import org.springframework.data.repository.query.ParameterAccessor;
3434
import org.springframework.data.repository.query.Parameters;
@@ -39,6 +39,8 @@
3939
import org.springframework.util.Assert;
4040
import org.springframework.util.StringUtils;
4141

42+
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.ParameterBinding;
43+
4244
/**
4345
* String-based Query abstracting a query with parameter bindings.
4446
*
@@ -48,7 +50,7 @@
4850
class StringBasedQuery {
4951

5052
private final String query;
51-
private final Parameters<?, ?> parameters;
53+
private final LdapParameters parameters;
5254
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
5355
private final ExpressionDependencies expressionDependencies;
5456

@@ -59,7 +61,7 @@ class StringBasedQuery {
5961
* @param parameters must not be {@literal null}.
6062
* @param expressionParser must not be {@literal null}.
6163
*/
62-
public StringBasedQuery(String query, Parameters<?, ?> parameters, ValueExpressionDelegate expressionParser) {
64+
public StringBasedQuery(String query, LdapParameters parameters, ValueExpressionDelegate expressionParser) {
6365

6466
this.query = ParameterBindingParser.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
6567
this.queryParameterBindings, expressionParser);
@@ -298,15 +300,15 @@ public static String bind(String input, List<Object> parameters) {
298300
*/
299301
static class BindingContext {
300302

301-
private final Parameters<?, ?> parameters;
303+
private final LdapParameters parameters;
302304
private final ParameterAccessor parameterAccessor;
303305
private final List<ParameterBinding> bindings;
304306
private final Function<ValueExpression, Object> evaluator;
305307

306308
/**
307309
* Create new {@link BindingContext}.
308310
*/
309-
BindingContext(Parameters<?, ?> parameters, ParameterAccessor parameterAccessor, List<ParameterBinding> bindings,
311+
BindingContext(LdapParameters parameters, ParameterAccessor parameterAccessor, List<ParameterBinding> bindings,
310312
Function<ValueExpression, Object> evaluator) {
311313

312314
this.parameters = parameters;
@@ -356,11 +358,15 @@ private Object getParameterValueForBinding(ParameterBinding binding) {
356358
if (binding.isExpression()) {
357359
return evaluator.apply(binding.getRequiredExpression());
358360
}
359-
360361
Object value = binding.isNamed()
361362
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
362363
: parameterAccessor.getBindableValue(binding.getParameterIndex());
363-
return value == null ? null : LdapEncoder.filterEncode(value.toString());
364+
365+
if (value == null) {
366+
return null;
367+
}
368+
369+
return binding.getEncodedValue(parameters, value);
364370
}
365371

366372
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
@@ -407,6 +413,38 @@ static ParameterBinding named(String name) {
407413
return new ParameterBinding(-1, null, name);
408414
}
409415

416+
Object getEncodedValue(LdapParameters ldapParameters, Object value) {
417+
org.springframework.data.ldap.repository.LdapEncoder encoder = encoderForParameter(ldapParameters);
418+
if (encoder == null) {
419+
return LdapEncoder.filterEncode(value.toString());
420+
}
421+
return encoder.filterEncode(value.toString());
422+
}
423+
424+
425+
@Nullable
426+
org.springframework.data.ldap.repository.LdapEncoder encoderForParameter(LdapParameters ldapParameters) {
427+
for (LdapParameters.LdapParameter parameter : ldapParameters) {
428+
if (isByName(parameter) || isByIndex(parameter)) {
429+
LdapEncode ldapEncode = parameter.parameter.getParameterAnnotation(LdapEncode.class);
430+
if (ldapEncode == null) {
431+
return null;
432+
}
433+
Class<? extends org.springframework.data.ldap.repository.LdapEncoder> encoder = ldapEncode.value();
434+
return BeanUtils.instantiateClass(encoder);
435+
}
436+
}
437+
return null;
438+
}
439+
440+
private boolean isByIndex(LdapParameters.LdapParameter parameter) {
441+
return parameterIndex != -1 && parameter.getIndex() == parameterIndex;
442+
}
443+
444+
private boolean isByName(LdapParameters.LdapParameter parameter) {
445+
return parameterName != null && parameterName.equals(parameter.getName().orElse(null));
446+
}
447+
410448
boolean isNamed() {
411449
return (parameterName != null);
412450
}

src/test/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQueryUnitTests.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
*/
1616
package org.springframework.data.ldap.repository.query;
1717

18-
import static org.assertj.core.api.Assertions.*;
19-
2018
import java.util.List;
2119

2220
import org.junit.jupiter.api.Test;
2321
import org.mockito.Mockito;
2422

2523
import org.springframework.data.ldap.core.mapping.LdapMappingContext;
24+
import org.springframework.data.ldap.repository.LdapEncoder;
25+
import org.springframework.data.ldap.repository.LdapEncode;
2626
import org.springframework.data.ldap.repository.LdapRepository;
2727
import org.springframework.data.ldap.repository.Query;
2828
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -32,6 +32,8 @@
3232
import org.springframework.ldap.core.LdapOperations;
3333
import org.springframework.ldap.query.LdapQuery;
3434

35+
import static org.assertj.core.api.Assertions.assertThat;
36+
3537
/**
3638
* Unit tests for {@link AnnotatedLdapRepositoryQuery}
3739
*
@@ -79,6 +81,18 @@ void shouldEncodeBase() throws NoSuchMethodException {
7981
assertThat(ldapQuery.base()).hasToString("cn=John\\29");
8082
}
8183

84+
@Test
85+
void shouldEncodeWithCustomEncoder() throws NoSuchMethodException {
86+
87+
LdapQueryMethod method = queryMethod("customEncoder", String.class);
88+
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
89+
90+
LdapQuery ldapQuery = query.createQuery(
91+
new LdapParametersParameterAccessor(method, new Object[] { "Doe" }));
92+
93+
assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=Doebar)");
94+
}
95+
8296
private LdapQueryMethod queryMethod(String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
8397
return new LdapQueryMethod(QueryRepository.class.getMethod(methodName, parameterTypes),
8498
new DefaultRepositoryMetadata(QueryRepository.class), new SpelAwareProxyProjectionFactory());
@@ -100,5 +114,16 @@ interface QueryRepository extends LdapRepository<SchemaEntry> {
100114
@Query(base = ":dc", value = "(cn=:fullName)")
101115
List<SchemaEntry> baseNamedParameters(String fullName, String dc);
102116

117+
@Query(value = "(cn=:fullName)")
118+
List<SchemaEntry> customEncoder(@LdapEncode(MyEncoder.class) String fullName);
119+
120+
}
121+
122+
static class MyEncoder implements LdapEncoder {
123+
124+
@Override
125+
public String filterEncode(String value) {
126+
return value + "bar";
127+
}
103128
}
104129
}

0 commit comments

Comments
 (0)