Polishing.

Refactor ContextualValueExpressionEvaluator into Function, refactor BindingContext into nested class.

Simplify tests. Rename tests to reflect what they are actually doing. Reformat code.

See #453
Original pull request: #505
This commit is contained in:
Mark Paluch
2024-10-23 09:25:07 +02:00
parent b88ce592d6
commit c9a45fb33f
15 changed files with 395 additions and 396 deletions

View File

@@ -13,10 +13,12 @@
** xref:repositories/namespace-reference.adoc[]
** xref:repositories/query-keywords-reference.adoc[]
** xref:repositories/query-return-types-reference.adoc[]
* xref:ldap.adoc[]
** xref:ldap/configuration.adoc[]
** xref:ldap/usage.adoc[]
** xref:ldap/query-methods.adoc[]
** xref:ldap/value-expressions.adoc[]
** xref:ldap/querydsl.adoc[]
** xref:ldap/cdi-integration.adoc[]

View File

@@ -76,3 +76,91 @@ The following table provides samples of the keywords that you can use with query
| `(!(Firstname=name))`
|===
[[ldap.query-methods.at-query]]
== Using `@Query`
If you need to use a custom query that can't be derived from the method name, you can use the `@Query` annotation to define the query.
As queries are tied to the Java method that runs them, you can actually bind parameters to be passed to the query.
The following example shows a query created with the `@Query` annotation:
.Declare query at the query method using `@Query`
====
[source,java]
----
interface PersonRepository extends LdapRepository<Person, Long> {
@Query("(&(employmentType=*)(!(employmentType=Hired))(mail=:emailAddress))")
Person findEmployeeByEmailAddress(String emailAddress);
}
----
====
NOTE: Spring Data supports named (parameter names prefixed with `:`) and positional parameter binding (in the form of zero-based `?0`).
We recommend using named parameters for easier readability.
Also, using positional parameters makes query methods a little error-prone when refactoring regarding the parameter position.
[[ldap.encoding]]
== Parameter Encoding
Query parameters of String-based queries are encoded according to https://datatracker.ietf.org/doc/html/rfc2254[RFC2254].
This can lead to undesired escaping of certain characters.
You can specify your own encoder through the `@LdapEncode` annotation that defines which javadoc:org.springframework.data.ldap.repository.LdapEncoder[] to use.
`@LdapEncode` applies to individual parameters of a query method.
It is not applies for derived queries or Value Expressions (SpEL, Property Placeholders).
.Declare a custom `LdapEncoder` for a query method
====
[source,java]
----
interface PersonRepository extends LdapRepository<Person, Long> {
@Query("(&(employmentType=*)(!(employmentType=Hired))(firstName=:firstName))")
Person findEmployeeByFirstNameLike(@LdapEncode(MyLikeEncoder.class) String firstName);
}
----
====
[[ldap.query.spel-expressions]]
== Using SpEL Expressions
Spring Data allows you to use SpEL expressions in your query methods.
SpEL expressions are part of Spring Data's xref:ldap/value-expressions.adoc[Value Expressions] support.
SpEL expressions can be used to manipulate query method arguments as well as to invoke bean methods.
Method arguments can be accessed by name or index as demonstrated in the following example.
.Using SpEL expressions in Repository Query Methods
====
[source,java]
----
@Query("(&(firstName=?#{[0]})(mail=:?#{principal.emailAddress}))")
List<Person> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
----
====
NOTE: Values provided by SpEL expressions are not escaped according to RFC2254.
You have to ensure that the values are properly escaped if needed.
Consider using Spring Ldap's `org.springframework.ldap.support.LdapEncoder` helper class.
[[ldap.query.property-placeholders]]
== Using Property Placeholders
Property Placeholders (see xref:ldap/value-expressions.adoc[Value Expressions]) can help to easily customize your queries based on configuration properties from Spring's `Environment`.
These are useful for queries that need to be customized based on the environment or configuration.
.Using Property Placeholders in Repository Query Methods
====
[source,java]
----
@Query("(&(firstName=?0)(stage=:?${myapp.stage:dev}))")
List<Person> findByFirstnameAndStageWithCustomQuery(String firstname);
----
====
NOTE: Values provided by Property Placeholders are not escaped according to RFC2254.
You have to ensure that the values are properly escaped if needed.
Consider using Spring Ldap's `org.springframework.ldap.support.LdapEncoder` helper class.

View File

@@ -0,0 +1 @@
include::{commons}@data-commons::page$value-expressions.adoc[]

View File

@@ -26,7 +26,6 @@ import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.util.Assert;

View File

@@ -39,9 +39,9 @@ import org.springframework.util.Assert;
public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
private final Query queryAnnotation;
private final ValueExpressionDelegate valueExpressionDelegate;
private final StringBasedQuery stringBasedQuery;
private final StringBasedQuery stringBasedBase;
private final StringBasedQuery query;
private final StringBasedQuery base;
private final ValueEvaluationContextProvider valueContextProvider;
/**
* Construct a new instance.
@@ -70,7 +70,7 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
* @param mappingContext must not be {@literal null}.
* @param instantiators must not be {@literal null}.
* @param valueExpressionDelegate must not be {@literal null}
* @since 3.4
* @since 3.5
*/
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
@@ -81,34 +81,32 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
Assert.notNull(queryMethod.getQueryAnnotation(), "Annotation must be present");
Assert.hasLength(queryMethod.getQueryAnnotation().value(), "Query filter must be specified");
queryAnnotation = queryMethod.getRequiredQueryAnnotation();
this.valueExpressionDelegate = valueExpressionDelegate;
stringBasedQuery = new StringBasedQuery(queryAnnotation.value(), queryMethod.getParameters(), valueExpressionDelegate);
stringBasedBase = new StringBasedQuery(queryAnnotation.base(), queryMethod.getParameters(), valueExpressionDelegate);
this.queryAnnotation = queryMethod.getRequiredQueryAnnotation();
this.query = new StringBasedQuery(queryAnnotation.value(), queryMethod.getParameters(), valueExpressionDelegate);
this.base = new StringBasedQuery(queryAnnotation.base(), queryMethod.getParameters(), valueExpressionDelegate);
this.valueContextProvider = valueExpressionDelegate.createValueContextProvider(getQueryMethod().getParameters());
}
@Override
protected LdapQuery createQuery(LdapParameterAccessor parameters) {
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
.createValueContextProvider(getQueryMethod().getParameters());
String query = bind(parameters, valueContextProvider, this.query);
String base = bind(parameters, valueContextProvider, this.base);
String boundQuery = bind(parameters, valueContextProvider, stringBasedQuery);
String boundBase = bind(parameters, valueContextProvider, stringBasedBase);
return query().base(boundBase) //
return query().base(base) //
.searchScope(queryAnnotation.searchScope()) //
.countLimit(queryAnnotation.countLimit()) //
.timeLimit(queryAnnotation.timeLimit()) //
.filter(boundQuery);
.filter(query, parameters.getBindableParameterValues());
}
private String bind(LdapParameterAccessor parameters, ValueEvaluationContextProvider valueContextProvider, StringBasedQuery query) {
ValueEvaluationContext evaluationContext = valueContextProvider
.getEvaluationContext(parameters.getBindableParameterValues(), query.getExpressionDependencies());
return query.bindQuery(parameters,
new ContextualValueExpressionEvaluator(valueExpressionDelegate, evaluationContext));
expression -> expression.evaluate(evaluationContext));
}
}

View File

@@ -1,177 +0,0 @@
/*
* Copyright 2020-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.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.lang.Nullable;
import org.springframework.ldap.support.LdapEncoder;
import org.springframework.util.Assert;
/**
* Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries.
*
* @author Mark Paluch
* @since 3.4
*/
class BindingContext {
private final Parameters<?, ?> parameters;
private final ParameterAccessor parameterAccessor;
private final List<ParameterBinding> bindings;
private final ValueExpressionEvaluator evaluator;
/**
* Create new {@link BindingContext}.
*/
BindingContext(Parameters<?, ?> parameters, ParameterAccessor parameterAccessor,
List<ParameterBinding> bindings, ValueExpressionEvaluator evaluator) {
this.parameters = parameters;
this.parameterAccessor = parameterAccessor;
this.bindings = bindings;
this.evaluator = evaluator;
}
/**
* @return {@literal true} when list of bindings is not empty.
*/
private boolean hasBindings() {
return !bindings.isEmpty();
}
/**
* Bind values provided by {@link LdapParameterAccessor} to placeholders in {@link BindingContext} while
* considering potential conversions and parameter types.
*
* @return {@literal null} if given {@code raw} value is empty.
*/
public List<Object> getBindingValues() {
if (!hasBindings()) {
return Collections.emptyList();
}
List<Object> parameters = new ArrayList<>(bindings.size());
for (ParameterBinding binding : bindings) {
Object parameterValueForBinding = getParameterValueForBinding(binding);
parameters.add(parameterValueForBinding);
}
return parameters;
}
/**
* Return the value to be used for the given {@link ParameterBinding}.
*
* @param binding must not be {@literal null}.
* @return the value used for the given {@link ParameterBinding}.
*/
@Nullable
private Object getParameterValueForBinding(ParameterBinding binding) {
if (binding.isExpression()) {
return evaluator.evaluate(binding.getRequiredExpression());
}
Object value = binding.isNamed() ?
parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName())) :
parameterAccessor.getBindableValue(binding.getParameterIndex());
return value == null ? null : LdapEncoder.filterEncode(value.toString());
}
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
return parameters.stream() //
.filter(parameter -> parameter //
.getName().filter(s -> s.equals(parameterName)) //
.isPresent()) //
.mapToInt(Parameter::getIndex) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName)));
}
/**
* A generic parameter binding with name or position information.
*
* @author Mark Paluch
*/
static class ParameterBinding {
private final int parameterIndex;
private final @Nullable String expression;
private final @Nullable String parameterName;
private ParameterBinding(int parameterIndex, @Nullable String expression, @Nullable String parameterName) {
this.parameterIndex = parameterIndex;
this.expression = expression;
this.parameterName = parameterName;
}
static ParameterBinding expression(String expression, boolean quoted) {
return new ParameterBinding(-1, expression, null);
}
static ParameterBinding indexed(int parameterIndex) {
return new ParameterBinding(parameterIndex, null, null);
}
static ParameterBinding named(String name) {
return new ParameterBinding(-1, null, name);
}
boolean isNamed() {
return (parameterName != null);
}
int getParameterIndex() {
return parameterIndex;
}
String getParameter() {
return ("?" + (isExpression() ? "expr" : "") + parameterIndex);
}
String getRequiredExpression() {
Assert.state(expression != null, "ParameterBinding is not an expression");
return expression;
}
boolean isExpression() {
return (this.expression != null);
}
String getRequiredParameterName() {
Assert.state(parameterName != null, "ParameterBinding is not named");
return parameterName;
}
}
}

View File

@@ -1,44 +0,0 @@
/*
* 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 org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
/**
* @author Marcin Grzejszczak
* @author Mark Paluch
*/
class ContextualValueExpressionEvaluator implements ValueExpressionEvaluator {
private final ValueExpressionParser parser;
public ContextualValueExpressionEvaluator(ValueExpressionParser parser, ValueEvaluationContext evaluationContext) {
this.parser = parser;
this.evaluationContext = evaluationContext;
}
private final ValueEvaluationContext evaluationContext;
@SuppressWarnings("unchecked")
@Override
public <T> T evaluate(String expressionString) {
ValueExpression expression = parser.parse(expressionString);
return (T) expression.evaluate(evaluationContext);
}
}

View File

@@ -23,7 +23,7 @@ import org.springframework.data.repository.query.ParameterAccessor;
* @author Mark Paluch
* @since 2.6
*/
interface LdapParameterAccessor extends ParameterAccessor {
public interface LdapParameterAccessor extends ParameterAccessor {
/**
* Returns the bindable parameter values of the underlying query method.

View File

@@ -15,20 +15,27 @@
*/
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;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.ldap.repository.query.BindingContext.ParameterBinding;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.lang.Nullable;
import org.springframework.ldap.support.LdapEncoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -36,18 +43,13 @@ import org.springframework.util.StringUtils;
* String-based Query abstracting a query with parameter bindings.
*
* @author Marcin Grzejszczak
* @since 3.4
* @since 3.5
*/
class StringBasedQuery {
private final String query;
private final Parameters<?, ?> parameters;
private final ValueExpressionDelegate expressionParser;
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
private final ExpressionDependencies expressionDependencies;
/**
@@ -57,12 +59,11 @@ class StringBasedQuery {
* @param parameters must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
*/
StringBasedQuery(String query, Parameters<?, ?> parameters, ValueExpressionDelegate expressionParser) {
public StringBasedQuery(String query, Parameters<?, ?> parameters, ValueExpressionDelegate expressionParser) {
this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
this.query = ParameterBindingParser.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings, expressionParser);
this.parameters = parameters;
this.expressionParser = expressionParser;
this.expressionDependencies = createExpressionDependencies();
}
@@ -77,7 +78,7 @@ class StringBasedQuery {
for (ParameterBinding binding : queryParameterBindings) {
if (binding.isExpression()) {
dependencies
.add(expressionParser.parse(binding.getRequiredExpression()).getExpressionDependencies());
.add(binding.getRequiredExpression().getExpressionDependencies());
}
}
@@ -100,17 +101,17 @@ class StringBasedQuery {
* @param evaluator must not be {@literal null}.
* @return the bound String query containing formatted parameters.
*/
String bindQuery(LdapParameterAccessor parameterAccessor, ValueExpressionEvaluator evaluator) {
public String bindQuery(LdapParameterAccessor parameterAccessor, Function<ValueExpression, Object> evaluator) {
Assert.notNull(parameterAccessor, "LdapParameterAccessor must not be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");
Assert.notNull(evaluator, "ExpressionEvaluator must not be null");
BindingContext bindingContext = new BindingContext(this.parameters, parameterAccessor, this.queryParameterBindings,
evaluator);
List<Object> arguments = bindingContext.getBindingValues();
return ParameterBinder.INSTANCE.bind(this.query, arguments);
return ParameterBinder.bind(this.query, arguments);
}
/**
@@ -118,54 +119,7 @@ class StringBasedQuery {
*
* @author Mark Paluch
*/
enum ParameterBinder {
INSTANCE;
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
public String bind(String input, List<Object> parameters) {
if (parameters.isEmpty()) {
return input;
}
StringBuilder result = new StringBuilder();
int startIndex = 0;
int currentPosition = 0;
int parameterIndex = 0;
Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input);
while (currentPosition < input.length()) {
if (!matcher.find()) {
break;
}
int exprStart = matcher.start();
result.append(input.subSequence(startIndex, exprStart)).append(parameters.get(parameterIndex));
parameterIndex++;
currentPosition = matcher.end();
startIndex = currentPosition;
}
return result.append(input.subSequence(currentPosition, input.length())).toString();
}
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Mark Paluch
*/
enum ParameterBindingParser {
INSTANCE;
static class ParameterBindingParser {
private static final char CURRLY_BRACE_OPEN = '{';
private static final char CURRLY_BRACE_CLOSE = '}';
@@ -177,7 +131,9 @@ class StringBasedQuery {
private static final Pattern INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\?\\$\\{");
private static final Pattern NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN = Pattern.compile("\\:\\$\\{");
private static final Set<Pattern> VALUE_EXPRESSION_PATTERNS = Set.of(INDEX_BASED_EXPRESSION_PATTERN, NAME_BASED_EXPRESSION_PATTERN, INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN, NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN);
private static final Set<Pattern> VALUE_EXPRESSION_PATTERNS = Set.of(INDEX_BASED_EXPRESSION_PATTERN,
NAME_BASED_EXPRESSION_PATTERN, INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN,
NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN);
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
@@ -186,9 +142,11 @@ class StringBasedQuery {
*
* @param input can be {@literal null} or empty.
* @param bindings must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @return a list of {@link ParameterBinding}s found in the given {@code input}.
*/
public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List<ParameterBinding> bindings) {
public static String parseAndCollectParameterBindingsFromQueryIntoBindings(String input,
List<ParameterBinding> bindings, ValueExpressionParser expressionParser) {
if (!StringUtils.hasText(input)) {
return input;
@@ -196,11 +154,11 @@ class StringBasedQuery {
Assert.notNull(bindings, "Parameter bindings must not be null");
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings, expressionParser);
}
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
List<ParameterBinding> bindings) {
List<ParameterBinding> bindings, ValueExpressionParser expressionParser) {
StringBuilder result = new StringBuilder();
@@ -243,13 +201,11 @@ class StringBasedQuery {
result.append(ARGUMENT_PLACEHOLDER);
if (isValueExpression(matcher)) {
bindings.add(
ParameterBinding
.expression(input.substring(exprStart + 1, currentPosition), true));
bindings.add(ParameterBinding
.expression(expressionParser.parse(input.substring(exprStart + 1, currentPosition)), true));
} else {
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
bindings
.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
bindings.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
} else {
bindings.add(ParameterBinding.named(matcher.group(1)));
}
@@ -290,4 +246,191 @@ class StringBasedQuery {
return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
}
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Mark Paluch
*/
static class ParameterBinder {
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
public static String bind(String input, List<Object> parameters) {
if (parameters.isEmpty()) {
return input;
}
StringBuilder result = new StringBuilder();
int startIndex = 0;
int currentPosition = 0;
int parameterIndex = 0;
Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input);
while (currentPosition < input.length()) {
if (!matcher.find()) {
break;
}
int exprStart = matcher.start();
result.append(input.subSequence(startIndex, exprStart)).append(parameters.get(parameterIndex));
parameterIndex++;
currentPosition = matcher.end();
startIndex = currentPosition;
}
return result.append(input.subSequence(currentPosition, input.length())).toString();
}
}
/**
* Value object capturing the binding context to provide {@link #getBindingValues() binding values} for queries.
*
* @author Mark Paluch
*/
static class BindingContext {
private final Parameters<?, ?> parameters;
private final ParameterAccessor parameterAccessor;
private final List<ParameterBinding> bindings;
private final Function<ValueExpression, Object> evaluator;
/**
* Create new {@link BindingContext}.
*/
BindingContext(Parameters<?, ?> parameters, ParameterAccessor parameterAccessor, List<ParameterBinding> bindings,
Function<ValueExpression, Object> evaluator) {
this.parameters = parameters;
this.parameterAccessor = parameterAccessor;
this.bindings = bindings;
this.evaluator = evaluator;
}
/**
* @return {@literal true} when list of bindings is not empty.
*/
private boolean hasBindings() {
return !bindings.isEmpty();
}
/**
* Bind values provided by {@link LdapParameterAccessor} to placeholders in {@link BindingContext} while considering
* potential conversions and parameter types.
*
* @return {@literal null} if given {@code raw} value is empty.
*/
public List<Object> getBindingValues() {
if (!hasBindings()) {
return Collections.emptyList();
}
List<Object> parameters = new ArrayList<>(bindings.size());
for (ParameterBinding binding : bindings) {
Object parameterValueForBinding = getParameterValueForBinding(binding);
parameters.add(parameterValueForBinding);
}
return parameters;
}
/**
* Return the value to be used for the given {@link ParameterBinding}.
*
* @param binding must not be {@literal null}.
* @return the value used for the given {@link ParameterBinding}.
*/
@Nullable
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());
}
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
for (Parameter parameter : parameters) {
if (parameter.getName().filter(it -> it.equals(parameterName)).isPresent()) {
return parameter.getIndex();
}
}
throw new IllegalArgumentException(
String.format("Invalid parameter name; Cannot resolve parameter [%s]", parameterName));
}
/**
* A generic parameter binding with name or position information.
*
* @author Mark Paluch
*/
static class ParameterBinding {
private final int parameterIndex;
private final @Nullable ValueExpression expression;
private final @Nullable String parameterName;
private ParameterBinding(int parameterIndex, @Nullable ValueExpression expression,
@Nullable String parameterName) {
this.parameterIndex = parameterIndex;
this.expression = expression;
this.parameterName = parameterName;
}
static ParameterBinding expression(ValueExpression expression, boolean quoted) {
return new ParameterBinding(-1, expression, null);
}
static ParameterBinding indexed(int parameterIndex) {
return new ParameterBinding(parameterIndex, null, null);
}
static ParameterBinding named(String name) {
return new ParameterBinding(-1, null, name);
}
boolean isNamed() {
return (parameterName != null);
}
int getParameterIndex() {
return parameterIndex;
}
ValueExpression getRequiredExpression() {
Assert.state(expression != null, "ParameterBinding is not an expression");
return expression;
}
boolean isExpression() {
return (this.expression != null);
}
String getRequiredParameterName() {
Assert.state(parameterName != null, "ParameterBinding is not named");
return parameterName;
}
}
}
}

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.ldap.config;
import javax.naming.Name;

View File

@@ -1,3 +1,18 @@
/*
* 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.config;
import java.util.ArrayList;

View File

@@ -74,11 +74,9 @@ public class InMemoryLdapConfiguration {
return this.server;
}
@Bean
@DependsOn("directoryServer")
LdapContextSource ldapContextSource(Environment environment, EmbeddedLdapProperties properties,
EmbeddedLdapProperties embeddedProperties) {
LdapContextSource ldapContextSource(Environment environment, EmbeddedLdapProperties properties) {
LdapContextSource source = new LdapContextSource();
Assert.notEmpty(properties.getBaseDn(), "Base DN must be set with at least one value");
source.setBase(properties.getBaseDn().get(0));

View File

@@ -31,7 +31,6 @@ import org.springframework.data.ldap.config.InMemoryLdapConfiguration;
import org.springframework.data.ldap.repository.LdapRepository;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.ldap.repository.config.EnableLdapRepositories;
import org.springframework.data.repository.query.Param;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@@ -41,79 +40,47 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
* @author Marcin Grzejszczak
*/
@SpringJUnitConfig
@TestPropertySource(properties = { "full.name=John Doe", "dc.name=memorynotfound" })
class ValueExpressionLdapRepositoryQueryTests {
@TestPropertySource(properties = { "full.name=John Doe" })
class AnnotatedLdapRepositoryQueryIntegrationTests {
@Autowired private QueryRepository queryRepository;
@Autowired QueryRepository queryRepository;
@Test
void shouldWorkWithNamedParameters() {
@Test // GH-453
void filterWithNamedParameters() {
List<SchemaEntry> objects = queryRepository.namedParameters("John Doe", "Bar");
List<SchemaEntry> objects = queryRepository.namedParameters("John Doe");
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void usingQueryLanguageCharsShouldNotFail() {
@Test // GH-453
void filterWithPositionalParameters() {
List<SchemaEntry> objects = queryRepository.namedParameters("John)(cn=Doe)", "Bar");
assertThat(objects).isEmpty();
}
@Test
void shouldWorkWithIndexParameters() {
List<SchemaEntry> objects = queryRepository.indexedParameters("John Doe", "Bar");
List<SchemaEntry> objects = queryRepository.indexedParameters("John Doe");
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithSpelExpressions() {
@Test // GH-453
void filterWithSpelExpression() {
List<SchemaEntry> objects = queryRepository.spelParameters();
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithPropertyPlaceholders() {
@Test // GH-453
void filterWithPropertyPlaceholder() {
List<SchemaEntry> objects = queryRepository.propertyPlaceholderParameters();
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithNamedParametersForBase() {
@Test // GH-453
void baseWithNamedParameters() {
List<SchemaEntry> objects = queryRepository.baseNamedParameters("John Doe", "dc=memorynotfound");
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithIndexParametersForBase() {
List<SchemaEntry> objects = queryRepository.baseIndexedParameters("John Doe", "memorynotfound");
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithSpelExpressionsForBase() {
List<SchemaEntry> objects = queryRepository.baseSpelParameters();
assertThatReturnedObjectIsJohnDoe(objects);
}
@Test
void shouldWorkWithPropertyPlaceholdersForBase() {
List<SchemaEntry> objects = queryRepository.basePropertyPlaceholderParameters();
List<SchemaEntry> objects = queryRepository.baseNamedParameters("John Doe", "memorynotfound");
assertThatReturnedObjectIsJohnDoe(objects);
}
@@ -141,10 +108,10 @@ class ValueExpressionLdapRepositoryQueryTests {
interface QueryRepository extends LdapRepository<SchemaEntry> {
@Query(value = "(cn=:fullName)")
List<SchemaEntry> namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName);
List<SchemaEntry> namedParameters(String fullName);
@Query(value = "(cn=?0)")
List<SchemaEntry> indexedParameters(String fullName, String lastName);
List<SchemaEntry> indexedParameters(String fullName);
@Query(value = "(cn=:#{'John ' + 'Doe'})")
List<SchemaEntry> spelParameters();
@@ -152,16 +119,7 @@ class ValueExpressionLdapRepositoryQueryTests {
@Query(value = "(cn=?${full.name})")
List<SchemaEntry> propertyPlaceholderParameters();
@Query(base = ":dc", value = "(cn=:fullName)")
List<SchemaEntry> baseNamedParameters(@Param("fullName") String fullName, @Param("dc") String dc);
@Query(base = "dc=?1", value = "(cn=?0)")
List<SchemaEntry> baseIndexedParameters(String fullName, String dc);
@Query(base = "dc=:#{'memory' + 'notfound'}", value = "(cn=:#{'John ' + 'Doe'})")
List<SchemaEntry> baseSpelParameters();
@Query(base = "dc=?${dc.name}", value = "(cn=?${full.name})")
List<SchemaEntry> basePropertyPlaceholderParameters();
@Query(base = "dc=:dc", value = "(cn=:fullName)")
List<SchemaEntry> baseNamedParameters(String fullName, String dc);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2024 the original author or authors.
* 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.
@@ -15,6 +15,8 @@
*/
package org.springframework.data.ldap.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -26,14 +28,17 @@ import org.springframework.data.ldap.repository.Query;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.query.Param;
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;
class AnnotatedLdapRepositoryQueryTests {
/**
* Unit tests for {@link AnnotatedLdapRepositoryQuery}
*
* @author Marcin Grzejszczak
* @author Mark Paluch
*/
class AnnotatedLdapRepositoryQueryUnitTests {
LdapOperations ldapOperations = Mockito.mock();
@@ -41,18 +46,31 @@ class AnnotatedLdapRepositoryQueryTests {
@Test
void shouldEncodeQuery() throws NoSuchMethodException {
LdapQueryMethod method = queryMethod("namedParameters");
LdapQueryMethod method = queryMethod("namedParameters", String.class);
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
LdapQuery ldapQuery = query.createQuery(
new LdapParametersParameterAccessor(method, new Object[] { "John)(cn=Doe)", "foo" }));
new LdapParametersParameterAccessor(method, new Object[] { "John)(cn=Doe)" }));
assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=John\\29\\28cn=Doe\\29)");
}
@Test
void messageFormatParametersShouldWork() throws NoSuchMethodException {
LdapQueryMethod method = queryMethod("messageFormatParameters", String.class);
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
LdapQuery ldapQuery = query.createQuery(new LdapParametersParameterAccessor(method, new Object[] { "John" }));
assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=John)");
}
@Test
void shouldEncodeBase() throws NoSuchMethodException {
LdapQueryMethod method = queryMethod("baseNamedParameters");
LdapQueryMethod method = queryMethod("baseNamedParameters", String.class, String.class);
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
LdapQuery ldapQuery = query.createQuery(
@@ -61,8 +79,8 @@ class AnnotatedLdapRepositoryQueryTests {
assertThat(ldapQuery.base()).hasToString("cn=John\\29");
}
private LdapQueryMethod queryMethod(String methodName) throws NoSuchMethodException {
return new LdapQueryMethod(QueryRepository.class.getMethod(methodName, String.class, String.class),
private LdapQueryMethod queryMethod(String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
return new LdapQueryMethod(QueryRepository.class.getMethod(methodName, parameterTypes),
new DefaultRepositoryMetadata(QueryRepository.class), new SpelAwareProxyProjectionFactory());
}
@@ -74,10 +92,13 @@ class AnnotatedLdapRepositoryQueryTests {
interface QueryRepository extends LdapRepository<SchemaEntry> {
@Query(value = "(cn=:fullName)")
List<SchemaEntry> namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName);
List<SchemaEntry> namedParameters(String fullName);
@Query(value = "(cn={0})")
List<SchemaEntry> messageFormatParameters(String fullName);
@Query(base = ":dc", value = "(cn=:fullName)")
List<SchemaEntry> baseNamedParameters(@Param("fullName") String fullName, @Param("dc") String dc);
List<SchemaEntry> baseNamedParameters(String fullName, String dc);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2024 the original author or authors.
* 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.
@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.ldap.repository.query;
import javax.naming.Name;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;
@@ -27,8 +25,8 @@ import org.springframework.ldap.odm.annotations.Id;
* @author Marcin Grzejszczak
*/
@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })
public class SchemaEntry {
@Id
Name dn;