Skip to content

Commit c858f46

Browse files
committed
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
1 parent de3b8ff commit c858f46

File tree

6 files changed

+170
-62
lines changed

6 files changed

+170
-62
lines changed

src/main/java/org/springframework/data/ldap/repository/LdapEncode.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,42 @@
2424
import org.springframework.core.annotation.AliasFor;
2525

2626
/**
27-
* Allows passing of custom {@link LdapEncoder}.
27+
* Annotation which indicates that a method parameter should be encoded using a specific {@link LdapEncoder} for a
28+
* repository query method invocation.
29+
* <p>
30+
* If no {@link LdapEncoder} is configured, bound method parameters are encoded using
31+
* {@link org.springframework.ldap.support.LdapEncoder#filterEncode(String)}. The default encoder considers chars such
32+
* as {@code *} (asterisk) to be encoded which might interfere with the intent of running a Like query. Since Spring
33+
* Data LDAP doesn't parse queries it is up to you to decide which encoder to use.
34+
* <p>
35+
* {@link LdapEncoder} implementations must declare a no-args constructor so they can be instantiated during repository
36+
* initialization.
37+
* <p>
38+
* Note that parameter encoding applies only to parameters that are directly bound to a query. Parameters used in Value
39+
* Expressions (SpEL, Configuration Properties) are not considered for encoding and must be encoded properly by using
40+
* SpEL Method invocations or a SpEL Extension.
2841
*
2942
* @author Marcin Grzejszczak
30-
* @since 3.5.0
43+
* @author Mark Paluch
44+
* @since 3.5
45+
* @see LdapEncoder.LikeEncoder
46+
* @see LdapEncoder.NameEncoder
3147
*/
32-
@Target(ElementType.PARAMETER)
48+
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
3349
@Retention(RetentionPolicy.RUNTIME)
3450
@Documented
3551
public @interface LdapEncode {
3652

3753
/**
38-
* {@link LdapEncoder} to instantiate to encode query parameters.
54+
* {@link LdapEncoder} to encode query parameters.
3955
*
4056
* @return {@link LdapEncoder} class
4157
*/
4258
@AliasFor("encoder")
4359
Class<? extends LdapEncoder> value();
4460

4561
/**
46-
* {@link LdapEncoder} to instantiate to encode query parameters.
62+
* {@link LdapEncoder} to encode query parameters.
4763
*
4864
* @return {@link LdapEncoder} class
4965
*/

src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java

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

18+
import org.springframework.util.ObjectUtils;
19+
1820
/**
19-
* Allows plugging in custom encoding for {@link LdapEncode}.
21+
* Strategy interface to escape values for use in LDAP filters.
22+
* <p>
23+
* Accepts an LDAP filter value to be encoded (escaped) for String-based LDAP query usage as LDAP queries do not feature
24+
* an out-of-band parameter binding mechanism.
25+
* <p>
26+
* Make sure that your implementation escapes special characters in the value adequately to prevent injection attacks.
2027
*
2128
* @author Marcin Grzejszczak
22-
* @since 3.5.0
29+
* @author Mark Paluch
30+
* @since 3.5
2331
*/
2432
public interface LdapEncoder {
2533

2634
/**
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.
35+
* Encode a value for use in a filter.
36+
*
37+
* @param value the value to encode.
38+
* @return a properly encoded representation of the supplied value.
39+
*/
40+
String encode(String value);
41+
42+
/**
43+
* {@link LdapEncoder} using {@link org.springframework.ldap.support.LdapEncoder#nameEncode(String)}. Encodes a value
44+
* for use with a DN. Escapes for LDAP, not JNDI!
45+
*/
46+
class NameEncoder implements LdapEncoder {
47+
48+
@Override
49+
public String encode(String value) {
50+
return org.springframework.ldap.support.LdapEncoder.nameEncode(value);
51+
}
52+
}
53+
54+
/**
55+
* Escape a value for use in a filter retaining asterisks ({@code *}) for like/contains searches.
3056
*/
31-
String filterEncode(String value);
57+
class LikeEncoder implements LdapEncoder {
58+
59+
@Override
60+
public String encode(String value) {
61+
62+
if (ObjectUtils.isEmpty(value)) {
63+
return value;
64+
}
65+
66+
String[] substrings = value.split("\\*", -2);
67+
68+
if (substrings.length == 1) {
69+
return org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[0]);
70+
}
71+
72+
StringBuilder buff = new StringBuilder();
73+
for (int i = 0; i < substrings.length; i++) {
74+
buff.append(org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[i]));
75+
if (i < substrings.length - 1) {
76+
buff.append("*");
77+
}
78+
}
79+
80+
return buff.toString();
81+
}
82+
}
83+
3284
}

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

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@
1818
import java.lang.reflect.Method;
1919
import java.util.List;
2020

21+
import org.springframework.beans.BeanUtils;
2122
import org.springframework.core.MethodParameter;
2223
import org.springframework.data.geo.Distance;
24+
import org.springframework.data.ldap.repository.LdapEncode;
25+
import org.springframework.data.ldap.repository.LdapEncoder;
2326
import org.springframework.data.repository.query.Parameter;
2427
import org.springframework.data.repository.query.Parameters;
2528
import org.springframework.data.repository.query.ParametersSource;
2629
import org.springframework.data.util.TypeInformation;
30+
import org.springframework.lang.Nullable;
2731

2832
/**
2933
* Custom extension of {@link Parameters} discovering additional
3034
*
3135
* @author Marcin Grzejszczak
32-
* @since 3.5.0
36+
* @since 3.5
3337
*/
3438
public class LdapParameters extends Parameters<LdapParameters, LdapParameters.LdapParameter> {
3539

@@ -65,9 +69,10 @@ protected LdapParameters createFrom(List<LdapParameter> parameters) {
6569
*
6670
* @author Marcin Grzejszczak
6771
*/
68-
static class LdapParameter extends Parameter {
72+
protected static class LdapParameter extends Parameter {
6973

70-
final MethodParameter parameter;
74+
private final @Nullable LdapEncoder ldapEncoder;
75+
private final MethodParameter parameter;
7176

7277
/**
7378
* Creates a new {@link LdapParameter}.
@@ -76,8 +81,29 @@ static class LdapParameter extends Parameter {
7681
* @param domainType must not be {@literal null}.
7782
*/
7883
LdapParameter(MethodParameter parameter, TypeInformation<?> domainType) {
84+
7985
super(parameter, domainType);
8086
this.parameter = parameter;
87+
88+
LdapEncode encode = parameter.getParameterAnnotation(LdapEncode.class);
89+
90+
if (encode != null) {
91+
this.ldapEncoder = BeanUtils.instantiateClass(encode.value());
92+
} else {
93+
this.ldapEncoder = null;
94+
}
95+
}
96+
97+
public boolean hasLdapEncoder() {
98+
return ldapEncoder != null;
99+
}
100+
101+
public LdapEncoder getLdapEncoder() {
102+
103+
if (ldapEncoder == null) {
104+
throw new IllegalStateException("No LdapEncoder found for parameter " + parameter);
105+
}
106+
return ldapEncoder;
81107
}
82108
}
83109

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

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2024 the original author or authors.
2+
* Copyright 2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.ldap.repository.query;
1717

18+
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.*;
19+
1820
import java.util.ArrayList;
1921
import java.util.Collections;
2022
import java.util.List;
@@ -25,10 +27,8 @@
2527
import java.util.regex.Matcher;
2628
import java.util.regex.Pattern;
2729

28-
import org.springframework.beans.BeanUtils;
2930
import org.springframework.data.expression.ValueExpression;
3031
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,12 +39,11 @@
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-
4442
/**
4543
* String-based Query abstracting a query with parameter bindings.
4644
*
4745
* @author Marcin Grzejszczak
46+
* @author Mark Paluch
4847
* @since 3.5
4948
*/
5049
class StringBasedQuery {
@@ -79,8 +78,7 @@ private ExpressionDependencies createExpressionDependencies() {
7978

8079
for (ParameterBinding binding : queryParameterBindings) {
8180
if (binding.isExpression()) {
82-
dependencies
83-
.add(binding.getRequiredExpression().getExpressionDependencies());
81+
dependencies.add(binding.getRequiredExpression().getExpressionDependencies());
8482
}
8583
}
8684

@@ -256,7 +254,6 @@ private static Matcher findNextBindingOrExpression(String input, int startPositi
256254
*/
257255
static class ParameterBinder {
258256

259-
260257
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
261258
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
262259

@@ -358,15 +355,20 @@ private Object getParameterValueForBinding(ParameterBinding binding) {
358355
if (binding.isExpression()) {
359356
return evaluator.apply(binding.getRequiredExpression());
360357
}
361-
Object value = binding.isNamed()
362-
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
363-
: parameterAccessor.getBindableValue(binding.getParameterIndex());
358+
359+
int index = binding.isNamed() ? getParameterIndex(parameters, binding.getRequiredParameterName())
360+
: binding.getParameterIndex();
361+
Object value = parameterAccessor.getBindableValue(index);
364362

365363
if (value == null) {
366364
return null;
367365
}
368366

369-
return binding.getEncodedValue(parameters, value);
367+
String toString = value.toString();
368+
LdapParameters.LdapParameter parameter = parameters.getBindableParameter(index);
369+
370+
return parameter.hasLdapEncoder() ? parameter.getLdapEncoder().encode(toString)
371+
: LdapEncoder.filterEncode(toString);
370372
}
371373

372374
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
@@ -413,38 +415,6 @@ static ParameterBinding named(String name) {
413415
return new ParameterBinding(-1, null, name);
414416
}
415417

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-
448418
boolean isNamed() {
449419
return (parameterName != null);
450420
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
/**
23+
* Unit tests for {@link LdapEncoder}.
24+
*
25+
* @author Mark Paluch
26+
*/
27+
class LdapEncoderUnitTests {
28+
29+
@Test // GH-509
30+
void shouldEncodeName() {
31+
32+
String result = new LdapEncoder.NameEncoder().encode("# foo ,+\"\\<>; ");
33+
34+
assertThat(result).isEqualTo("\\# foo \\,\\+\\\"\\\\\\<\\>\\;\\ ");
35+
}
36+
37+
@Test // GH-509
38+
void shouldEncodeLikeFilter() {
39+
40+
String result = new LdapEncoder.LikeEncoder().encode("*hugo*ern(o)*");
41+
42+
assertThat(result).isEqualTo("*hugo*ern\\28o\\29*");
43+
}
44+
}

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

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

18+
import static org.assertj.core.api.Assertions.*;
19+
1820
import java.util.List;
1921

2022
import org.junit.jupiter.api.Test;
2123
import org.mockito.Mockito;
2224

2325
import org.springframework.data.ldap.core.mapping.LdapMappingContext;
24-
import org.springframework.data.ldap.repository.LdapEncoder;
2526
import org.springframework.data.ldap.repository.LdapEncode;
27+
import org.springframework.data.ldap.repository.LdapEncoder;
2628
import org.springframework.data.ldap.repository.LdapRepository;
2729
import org.springframework.data.ldap.repository.Query;
2830
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -32,8 +34,6 @@
3234
import org.springframework.ldap.core.LdapOperations;
3335
import org.springframework.ldap.query.LdapQuery;
3436

35-
import static org.assertj.core.api.Assertions.assertThat;
36-
3737
/**
3838
* Unit tests for {@link AnnotatedLdapRepositoryQuery}
3939
*
@@ -122,7 +122,7 @@ interface QueryRepository extends LdapRepository<SchemaEntry> {
122122
static class MyEncoder implements LdapEncoder {
123123

124124
@Override
125-
public String filterEncode(String value) {
125+
public String encode(String value) {
126126
return value + "bar";
127127
}
128128
}

0 commit comments

Comments
 (0)