Skip to content

Commit b88ce59

Browse files
marcingrzejszczakmp911de
authored andcommitted
Add support for Value Expressions in @Query methods.
Closes #453 Original pull request: #505
1 parent c491b69 commit b88ce59

15 files changed

+1149
-24
lines changed

pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<spring-ldap>3.2.8</spring-ldap>
2222
<springdata.commons>3.5.0-SNAPSHOT</springdata.commons>
2323
<java-module-name>spring.data.ldap</java-module-name>
24+
<unboundid-ldapsdk>7.0.1</unboundid-ldapsdk>
2425
</properties>
2526

2627
<developers>
@@ -109,6 +110,20 @@
109110
<scope>test</scope>
110111
</dependency>
111112

113+
<dependency>
114+
<groupId>org.springframework.ldap</groupId>
115+
<artifactId>spring-ldap-test</artifactId>
116+
<version>${spring-ldap}</version>
117+
<scope>test</scope>
118+
</dependency>
119+
120+
<dependency>
121+
<groupId>com.unboundid</groupId>
122+
<artifactId>unboundid-ldapsdk</artifactId>
123+
<version>${unboundid-ldapsdk}</version>
124+
<scope>test</scope>
125+
</dependency>
126+
112127
</dependencies>
113128

114129
<build>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.repository.query.QueryMethod;
2727
import org.springframework.data.repository.query.RepositoryQuery;
2828
import org.springframework.data.repository.query.ResultProcessor;
29+
import org.springframework.data.repository.query.ValueExpressionDelegate;
2930
import org.springframework.ldap.core.LdapOperations;
3031
import org.springframework.ldap.query.LdapQuery;
3132
import org.springframework.util.Assert;

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717

1818
import static org.springframework.ldap.query.LdapQueryBuilder.*;
1919

20+
import org.springframework.data.expression.ValueEvaluationContext;
21+
import org.springframework.data.expression.ValueEvaluationContextProvider;
2022
import org.springframework.data.ldap.repository.Query;
2123
import org.springframework.data.mapping.PersistentEntity;
2224
import org.springframework.data.mapping.PersistentProperty;
2325
import org.springframework.data.mapping.context.MappingContext;
2426
import org.springframework.data.mapping.model.EntityInstantiators;
27+
import org.springframework.data.repository.query.ValueExpressionDelegate;
2528
import org.springframework.ldap.core.LdapOperations;
2629
import org.springframework.ldap.query.LdapQuery;
2730
import org.springframework.util.Assert;
@@ -31,10 +34,14 @@
3134
*
3235
* @author Mattias Hellborg Arthursson
3336
* @author Mark Paluch
37+
* @author Marcin Grzejszczak
3438
*/
3539
public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
3640

3741
private final Query queryAnnotation;
42+
private final ValueExpressionDelegate valueExpressionDelegate;
43+
private final StringBasedQuery stringBasedQuery;
44+
private final StringBasedQuery stringBasedBase;
3845

3946
/**
4047
* Construct a new instance.
@@ -44,26 +51,64 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
4451
* @param ldapOperations the LdapOperations instance to use.
4552
* @param mappingContext must not be {@literal null}.
4653
* @param instantiators must not be {@literal null}.
54+
* @deprecated use the constructor with {@link ValueExpressionDelegate}
4755
*/
56+
@Deprecated(since = "3.4")
4857
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
4958
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
5059
EntityInstantiators instantiators) {
5160

61+
this(queryMethod, entityType, ldapOperations, mappingContext, instantiators, ValueExpressionDelegate.create());
62+
}
63+
64+
/**
65+
* Construct a new instance.
66+
*
67+
* @param queryMethod the QueryMethod.
68+
* @param entityType the managed class.
69+
* @param ldapOperations the LdapOperations instance to use.
70+
* @param mappingContext must not be {@literal null}.
71+
* @param instantiators must not be {@literal null}.
72+
* @param valueExpressionDelegate must not be {@literal null}
73+
* @since 3.4
74+
*/
75+
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
76+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
77+
EntityInstantiators instantiators, ValueExpressionDelegate valueExpressionDelegate) {
78+
5279
super(queryMethod, entityType, ldapOperations, mappingContext, instantiators);
5380

5481
Assert.notNull(queryMethod.getQueryAnnotation(), "Annotation must be present");
5582
Assert.hasLength(queryMethod.getQueryAnnotation().value(), "Query filter must be specified");
5683

5784
queryAnnotation = queryMethod.getRequiredQueryAnnotation();
85+
this.valueExpressionDelegate = valueExpressionDelegate;
86+
stringBasedQuery = new StringBasedQuery(queryAnnotation.value(), queryMethod.getParameters(), valueExpressionDelegate);
87+
stringBasedBase = new StringBasedQuery(queryAnnotation.base(), queryMethod.getParameters(), valueExpressionDelegate);
5888
}
5989

6090
@Override
6191
protected LdapQuery createQuery(LdapParameterAccessor parameters) {
6292

63-
return query().base(queryAnnotation.base()) //
93+
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
94+
.createValueContextProvider(getQueryMethod().getParameters());
95+
96+
String boundQuery = bind(parameters, valueContextProvider, stringBasedQuery);
97+
98+
String boundBase = bind(parameters, valueContextProvider, stringBasedBase);
99+
100+
return query().base(boundBase) //
64101
.searchScope(queryAnnotation.searchScope()) //
65102
.countLimit(queryAnnotation.countLimit()) //
66103
.timeLimit(queryAnnotation.timeLimit()) //
67-
.filter(queryAnnotation.value(), parameters.getBindableParameterValues());
104+
.filter(boundQuery);
68105
}
106+
107+
private String bind(LdapParameterAccessor parameters, ValueEvaluationContextProvider valueContextProvider, StringBasedQuery query) {
108+
ValueEvaluationContext evaluationContext = valueContextProvider
109+
.getEvaluationContext(parameters.getBindableParameterValues(), query.getExpressionDependencies());
110+
return query.bindQuery(parameters,
111+
new ContextualValueExpressionEvaluator(valueExpressionDelegate, evaluationContext));
112+
}
113+
69114
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2020-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.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
22+
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
23+
import org.springframework.data.repository.query.Parameter;
24+
import org.springframework.data.repository.query.ParameterAccessor;
25+
import org.springframework.data.repository.query.Parameters;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.ldap.support.LdapEncoder;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries.
32+
*
33+
* @author Mark Paluch
34+
* @since 3.4
35+
*/
36+
class BindingContext {
37+
38+
private final Parameters<?, ?> parameters;
39+
40+
private final ParameterAccessor parameterAccessor;
41+
42+
private final List<ParameterBinding> bindings;
43+
44+
private final ValueExpressionEvaluator evaluator;
45+
46+
/**
47+
* Create new {@link BindingContext}.
48+
*/
49+
BindingContext(Parameters<?, ?> parameters, ParameterAccessor parameterAccessor,
50+
List<ParameterBinding> bindings, ValueExpressionEvaluator evaluator) {
51+
52+
this.parameters = parameters;
53+
this.parameterAccessor = parameterAccessor;
54+
this.bindings = bindings;
55+
this.evaluator = evaluator;
56+
}
57+
58+
/**
59+
* @return {@literal true} when list of bindings is not empty.
60+
*/
61+
private boolean hasBindings() {
62+
return !bindings.isEmpty();
63+
}
64+
65+
/**
66+
* Bind values provided by {@link LdapParameterAccessor} to placeholders in {@link BindingContext} while
67+
* considering potential conversions and parameter types.
68+
*
69+
* @return {@literal null} if given {@code raw} value is empty.
70+
*/
71+
public List<Object> getBindingValues() {
72+
73+
if (!hasBindings()) {
74+
return Collections.emptyList();
75+
}
76+
77+
List<Object> parameters = new ArrayList<>(bindings.size());
78+
79+
for (ParameterBinding binding : bindings) {
80+
Object parameterValueForBinding = getParameterValueForBinding(binding);
81+
parameters.add(parameterValueForBinding);
82+
}
83+
84+
return parameters;
85+
}
86+
87+
/**
88+
* Return the value to be used for the given {@link ParameterBinding}.
89+
*
90+
* @param binding must not be {@literal null}.
91+
* @return the value used for the given {@link ParameterBinding}.
92+
*/
93+
@Nullable
94+
private Object getParameterValueForBinding(ParameterBinding binding) {
95+
96+
if (binding.isExpression()) {
97+
return evaluator.evaluate(binding.getRequiredExpression());
98+
}
99+
100+
Object value = binding.isNamed() ?
101+
parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName())) :
102+
parameterAccessor.getBindableValue(binding.getParameterIndex());
103+
return value == null ? null : LdapEncoder.filterEncode(value.toString());
104+
}
105+
106+
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
107+
108+
return parameters.stream() //
109+
.filter(parameter -> parameter //
110+
.getName().filter(s -> s.equals(parameterName)) //
111+
.isPresent()) //
112+
.mapToInt(Parameter::getIndex) //
113+
.findFirst() //
114+
.orElseThrow(() -> new IllegalArgumentException(
115+
String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName)));
116+
}
117+
118+
/**
119+
* A generic parameter binding with name or position information.
120+
*
121+
* @author Mark Paluch
122+
*/
123+
static class ParameterBinding {
124+
125+
private final int parameterIndex;
126+
private final @Nullable String expression;
127+
private final @Nullable String parameterName;
128+
129+
private ParameterBinding(int parameterIndex, @Nullable String expression, @Nullable String parameterName) {
130+
131+
this.parameterIndex = parameterIndex;
132+
this.expression = expression;
133+
this.parameterName = parameterName;
134+
}
135+
136+
static ParameterBinding expression(String expression, boolean quoted) {
137+
return new ParameterBinding(-1, expression, null);
138+
}
139+
140+
static ParameterBinding indexed(int parameterIndex) {
141+
return new ParameterBinding(parameterIndex, null, null);
142+
}
143+
144+
static ParameterBinding named(String name) {
145+
return new ParameterBinding(-1, null, name);
146+
}
147+
148+
boolean isNamed() {
149+
return (parameterName != null);
150+
}
151+
152+
int getParameterIndex() {
153+
return parameterIndex;
154+
}
155+
156+
String getParameter() {
157+
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
158+
}
159+
160+
String getRequiredExpression() {
161+
162+
Assert.state(expression != null, "ParameterBinding is not an expression");
163+
return expression;
164+
}
165+
166+
boolean isExpression() {
167+
return (this.expression != null);
168+
}
169+
170+
String getRequiredParameterName() {
171+
172+
Assert.state(parameterName != null, "ParameterBinding is not named");
173+
174+
return parameterName;
175+
}
176+
}
177+
}
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.query;
17+
18+
import org.springframework.data.expression.ValueEvaluationContext;
19+
import org.springframework.data.expression.ValueExpression;
20+
import org.springframework.data.expression.ValueExpressionParser;
21+
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
22+
23+
/**
24+
* @author Marcin Grzejszczak
25+
* @author Mark Paluch
26+
*/
27+
class ContextualValueExpressionEvaluator implements ValueExpressionEvaluator {
28+
29+
private final ValueExpressionParser parser;
30+
31+
public ContextualValueExpressionEvaluator(ValueExpressionParser parser, ValueEvaluationContext evaluationContext) {
32+
this.parser = parser;
33+
this.evaluationContext = evaluationContext;
34+
}
35+
36+
private final ValueEvaluationContext evaluationContext;
37+
38+
@SuppressWarnings("unchecked")
39+
@Override
40+
public <T> T evaluate(String expressionString) {
41+
ValueExpression expression = parser.parse(expressionString);
42+
return (T) expression.evaluate(evaluationContext);
43+
}
44+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
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;
25+
import org.springframework.data.repository.query.ParametersSource;
2426
import org.springframework.data.repository.query.QueryMethod;
2527
import org.springframework.lang.Nullable;
2628

@@ -85,4 +87,5 @@ Query getRequiredQueryAnnotation() {
8587

8688
throw new IllegalStateException("Required @Query annotation is not present");
8789
}
90+
8891
}

0 commit comments

Comments
 (0)