Add support for Value Expressions in @Query methods.
Closes #453 Original pull request: #505
This commit is contained in:
committed by
Mark Paluch
parent
c491b69e08
commit
b88ce592d6
15
pom.xml
15
pom.xml
@@ -21,6 +21,7 @@
|
||||
<spring-ldap>3.2.8</spring-ldap>
|
||||
<springdata.commons>3.5.0-SNAPSHOT</springdata.commons>
|
||||
<java-module-name>spring.data.ldap</java-module-name>
|
||||
<unboundid-ldapsdk>7.0.1</unboundid-ldapsdk>
|
||||
</properties>
|
||||
|
||||
<developers>
|
||||
@@ -109,6 +110,20 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ldap</groupId>
|
||||
<artifactId>spring-ldap-test</artifactId>
|
||||
<version>${spring-ldap}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.unboundid</groupId>
|
||||
<artifactId>unboundid-ldapsdk</artifactId>
|
||||
<version>${unboundid-ldapsdk}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -26,6 +26,7 @@ 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;
|
||||
|
||||
@@ -17,11 +17,14 @@ package org.springframework.data.ldap.repository.query;
|
||||
|
||||
import static org.springframework.ldap.query.LdapQueryBuilder.*;
|
||||
|
||||
import org.springframework.data.expression.ValueEvaluationContext;
|
||||
import org.springframework.data.expression.ValueEvaluationContextProvider;
|
||||
import org.springframework.data.ldap.repository.Query;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.ldap.core.LdapOperations;
|
||||
import org.springframework.ldap.query.LdapQuery;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -31,10 +34,14 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* @author Mattias Hellborg Arthursson
|
||||
* @author Mark Paluch
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
|
||||
|
||||
private final Query queryAnnotation;
|
||||
private final ValueExpressionDelegate valueExpressionDelegate;
|
||||
private final StringBasedQuery stringBasedQuery;
|
||||
private final StringBasedQuery stringBasedBase;
|
||||
|
||||
/**
|
||||
* Construct a new instance.
|
||||
@@ -44,26 +51,64 @@ public class AnnotatedLdapRepositoryQuery extends AbstractLdapRepositoryQuery {
|
||||
* @param ldapOperations the LdapOperations instance to use.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
* @param instantiators must not be {@literal null}.
|
||||
* @deprecated use the constructor with {@link ValueExpressionDelegate}
|
||||
*/
|
||||
@Deprecated(since = "3.4")
|
||||
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
|
||||
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
|
||||
EntityInstantiators instantiators) {
|
||||
|
||||
this(queryMethod, entityType, ldapOperations, mappingContext, instantiators, ValueExpressionDelegate.create());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new instance.
|
||||
*
|
||||
* @param queryMethod the QueryMethod.
|
||||
* @param entityType the managed class.
|
||||
* @param ldapOperations the LdapOperations instance to use.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
* @param instantiators must not be {@literal null}.
|
||||
* @param valueExpressionDelegate must not be {@literal null}
|
||||
* @since 3.4
|
||||
*/
|
||||
public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class<?> entityType, LdapOperations ldapOperations,
|
||||
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
|
||||
EntityInstantiators instantiators, ValueExpressionDelegate valueExpressionDelegate) {
|
||||
|
||||
super(queryMethod, entityType, ldapOperations, mappingContext, instantiators);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LdapQuery createQuery(LdapParameterAccessor parameters) {
|
||||
|
||||
return query().base(queryAnnotation.base()) //
|
||||
ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate
|
||||
.createValueContextProvider(getQueryMethod().getParameters());
|
||||
|
||||
String boundQuery = bind(parameters, valueContextProvider, stringBasedQuery);
|
||||
|
||||
String boundBase = bind(parameters, valueContextProvider, stringBasedBase);
|
||||
|
||||
return query().base(boundBase) //
|
||||
.searchScope(queryAnnotation.searchScope()) //
|
||||
.countLimit(queryAnnotation.countLimit()) //
|
||||
.timeLimit(queryAnnotation.timeLimit()) //
|
||||
.filter(queryAnnotation.value(), parameters.getBindableParameterValues());
|
||||
.filter(boundQuery);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import org.springframework.core.annotation.AnnotationUtils;
|
||||
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;
|
||||
|
||||
@@ -85,4 +87,5 @@ public class LdapQueryMethod extends QueryMethod {
|
||||
|
||||
throw new IllegalStateException("Required @Query annotation is not present");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2016-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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
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.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.data.spel.ExpressionDependencies;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* String-based Query abstracting a query with parameter bindings.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.4
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Create a new {@link StringBasedQuery} given {@code query}, {@link Parameters} and {@link ValueExpressionDelegate}.
|
||||
*
|
||||
* @param query must not be empty.
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param expressionParser must not be {@literal null}.
|
||||
*/
|
||||
StringBasedQuery(String query, Parameters<?, ?> parameters, ValueExpressionDelegate expressionParser) {
|
||||
|
||||
this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
|
||||
this.queryParameterBindings);
|
||||
this.parameters = parameters;
|
||||
this.expressionParser = expressionParser;
|
||||
this.expressionDependencies = createExpressionDependencies();
|
||||
}
|
||||
|
||||
private ExpressionDependencies createExpressionDependencies() {
|
||||
|
||||
if (queryParameterBindings.isEmpty()) {
|
||||
return ExpressionDependencies.none();
|
||||
}
|
||||
|
||||
List<ExpressionDependencies> dependencies = new ArrayList<>();
|
||||
|
||||
for (ParameterBinding binding : queryParameterBindings) {
|
||||
if (binding.isExpression()) {
|
||||
dependencies
|
||||
.add(expressionParser.parse(binding.getRequiredExpression()).getExpressionDependencies());
|
||||
}
|
||||
}
|
||||
|
||||
return ExpressionDependencies.merged(dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain {@link ExpressionDependencies} from the parsed query.
|
||||
*
|
||||
* @return the {@link ExpressionDependencies} from the parsed query.
|
||||
*/
|
||||
public ExpressionDependencies getExpressionDependencies() {
|
||||
return expressionDependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the query to actual parameters using {@link LdapParameterAccessor},
|
||||
*
|
||||
* @param parameterAccessor must not be {@literal null}.
|
||||
* @param evaluator must not be {@literal null}.
|
||||
* @return the bound String query containing formatted parameters.
|
||||
*/
|
||||
String bindQuery(LdapParameterAccessor parameterAccessor, ValueExpressionEvaluator evaluator) {
|
||||
|
||||
Assert.notNull(parameterAccessor, "LdapParameterAccessor must not be null");
|
||||
Assert.notNull(evaluator, "SpELExpressionEvaluator 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that extracts the parameter bindings from a given query string.
|
||||
*
|
||||
* @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;
|
||||
|
||||
private static final char CURRLY_BRACE_OPEN = '{';
|
||||
private static final char CURRLY_BRACE_CLOSE = '}';
|
||||
|
||||
private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
|
||||
private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)");
|
||||
private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{");
|
||||
private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{");
|
||||
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 String ARGUMENT_PLACEHOLDER = "?_param_?";
|
||||
|
||||
/**
|
||||
* Returns a list of {@link ParameterBinding}s found in the given {@code input}.
|
||||
*
|
||||
* @param input can be {@literal null} or empty.
|
||||
* @param bindings 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) {
|
||||
|
||||
if (!StringUtils.hasText(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
Assert.notNull(bindings, "Parameter bindings must not be null");
|
||||
|
||||
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
|
||||
}
|
||||
|
||||
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
|
||||
List<ParameterBinding> bindings) {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
int startIndex = 0;
|
||||
int currentPosition = 0;
|
||||
|
||||
while (currentPosition < input.length()) {
|
||||
|
||||
Matcher matcher = findNextBindingOrExpression(input, currentPosition);
|
||||
|
||||
// no expression parameter found
|
||||
if (matcher == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
int exprStart = matcher.start();
|
||||
currentPosition = exprStart;
|
||||
|
||||
if (isValueExpression(matcher)) {
|
||||
// eat parameter expression
|
||||
int curlyBraceOpenCount = 1;
|
||||
currentPosition += 3;
|
||||
|
||||
while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
|
||||
switch (input.charAt(currentPosition++)) {
|
||||
case CURRLY_BRACE_OPEN:
|
||||
curlyBraceOpenCount++;
|
||||
break;
|
||||
case CURRLY_BRACE_CLOSE:
|
||||
curlyBraceOpenCount--;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
} else {
|
||||
result.append(input.subSequence(startIndex, exprStart));
|
||||
}
|
||||
|
||||
result.append(ARGUMENT_PLACEHOLDER);
|
||||
|
||||
if (isValueExpression(matcher)) {
|
||||
bindings.add(
|
||||
ParameterBinding
|
||||
.expression(input.substring(exprStart + 1, currentPosition), true));
|
||||
} else {
|
||||
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
|
||||
bindings
|
||||
.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
|
||||
} else {
|
||||
bindings.add(ParameterBinding.named(matcher.group(1)));
|
||||
}
|
||||
|
||||
currentPosition = matcher.end();
|
||||
}
|
||||
|
||||
startIndex = currentPosition;
|
||||
}
|
||||
|
||||
return result.append(input.subSequence(currentPosition, input.length())).toString();
|
||||
}
|
||||
|
||||
private static boolean isValueExpression(Matcher matcher) {
|
||||
return VALUE_EXPRESSION_PATTERNS.contains(matcher.pattern());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Matcher findNextBindingOrExpression(String input, int startPosition) {
|
||||
|
||||
List<Matcher> matchers = new ArrayList<>(6);
|
||||
|
||||
matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input));
|
||||
matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input));
|
||||
matchers.add(INDEX_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input));
|
||||
matchers.add(NAME_BASED_PROPERTY_PLACEHOLDER_PATTERN.matcher(input));
|
||||
|
||||
Map<Integer, Matcher> matcherMap = new TreeMap<>();
|
||||
|
||||
for (Matcher matcher : matchers) {
|
||||
if (matcher.find(startPosition)) {
|
||||
matcherMap.put(matcher.start(), matcher);
|
||||
}
|
||||
}
|
||||
|
||||
return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.ValueExpressionDelegate;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.ldap.core.LdapOperations;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -56,7 +57,6 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
private final LdapQueryLookupStrategy queryLookupStrategy;
|
||||
private final LdapOperations ldapOperations;
|
||||
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
|
||||
private final EntityInstantiators instantiators = new EntityInstantiators();
|
||||
@@ -72,7 +72,6 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
this.ldapOperations = ldapOperations;
|
||||
this.mappingContext = new LdapMappingContext();
|
||||
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +86,6 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
Assert.notNull(ldapOperations, "LdapOperations must not be null");
|
||||
Assert.notNull(mappingContext, "LdapMappingContext must not be null");
|
||||
|
||||
this.queryLookupStrategy = new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext);
|
||||
this.ldapOperations = ldapOperations;
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
@@ -154,9 +152,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
return Optional.of(queryLookupStrategy);
|
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key,
|
||||
ValueExpressionDelegate valueExpressionDelegate) {
|
||||
return Optional.of(new LdapQueryLookupStrategy(ldapOperations, instantiators, mappingContext, valueExpressionDelegate));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,19 +183,9 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
return acceptsMappingContext;
|
||||
}
|
||||
|
||||
private static final class LdapQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final LdapOperations ldapOperations;
|
||||
private final EntityInstantiators instantiators;
|
||||
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext;
|
||||
|
||||
public LdapQueryLookupStrategy(LdapOperations ldapOperations, EntityInstantiators instantiators,
|
||||
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext) {
|
||||
|
||||
this.ldapOperations = ldapOperations;
|
||||
this.instantiators = instantiators;
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
private record LdapQueryLookupStrategy(LdapOperations ldapOperations,
|
||||
EntityInstantiators instantiators, MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> mappingContext,
|
||||
ValueExpressionDelegate valueExpressionDelegate) implements QueryLookupStrategy {
|
||||
|
||||
@Override
|
||||
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
|
||||
@@ -207,7 +195,7 @@ public class LdapRepositoryFactory extends RepositoryFactorySupport {
|
||||
Class<?> domainType = metadata.getDomainType();
|
||||
|
||||
if (queryMethod.hasQueryAnnotation()) {
|
||||
return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators);
|
||||
return new AnnotatedLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators, valueExpressionDelegate);
|
||||
} else {
|
||||
return new PartTreeLdapRepositoryQuery(queryMethod, domainType, ldapOperations, mappingContext, instantiators);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.springframework.data.ldap.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmbeddedLdapProperties {
|
||||
|
||||
/**
|
||||
* Embedded LDAP port.
|
||||
*/
|
||||
private int port = 0;
|
||||
|
||||
/**
|
||||
* List of base DNs.
|
||||
*/
|
||||
private List<String> baseDn = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Schema (LDIF) script resource reference.
|
||||
*/
|
||||
private String ldif = "classpath:schema.ldif";
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public List<String> getBaseDn() {
|
||||
return this.baseDn;
|
||||
}
|
||||
|
||||
public void setBaseDn(List<String> baseDn) {
|
||||
this.baseDn = baseDn;
|
||||
}
|
||||
|
||||
public String getLdif() {
|
||||
return this.ldif;
|
||||
}
|
||||
|
||||
public void setLdif(String ldif) {
|
||||
this.ldif = ldif;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 jakarta.annotation.PreDestroy;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.ldap.core.LdapTemplate;
|
||||
import org.springframework.ldap.core.support.LdapContextSource;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
|
||||
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
|
||||
import com.unboundid.ldap.listener.InMemoryListenerConfig;
|
||||
import com.unboundid.ldap.sdk.LDAPException;
|
||||
import com.unboundid.ldif.LDIFReader;
|
||||
|
||||
/**
|
||||
* Taken from Spring Boot
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class InMemoryLdapConfiguration {
|
||||
|
||||
private static final String PROPERTY_SOURCE_NAME = "ldap.ports";
|
||||
|
||||
private InMemoryDirectoryServer server;
|
||||
|
||||
private final EmbeddedLdapProperties embeddedProperties;
|
||||
|
||||
public InMemoryLdapConfiguration(EmbeddedLdapProperties embeddedLdapProperties) {
|
||||
this.embeddedProperties = embeddedLdapProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext)
|
||||
throws LDAPException {
|
||||
String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn());
|
||||
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn);
|
||||
setSchema(config);
|
||||
InMemoryListenerConfig listenerConfig = InMemoryListenerConfig.createLDAPConfig("LDAP",
|
||||
this.embeddedProperties.getPort());
|
||||
config.setListenerConfigs(listenerConfig);
|
||||
this.server = new InMemoryDirectoryServer(config);
|
||||
importLdif(applicationContext);
|
||||
this.server.startListening();
|
||||
setPortProperty(applicationContext, this.server.getListenPort());
|
||||
return this.server;
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@DependsOn("directoryServer")
|
||||
LdapContextSource ldapContextSource(Environment environment, EmbeddedLdapProperties properties,
|
||||
EmbeddedLdapProperties embeddedProperties) {
|
||||
LdapContextSource source = new LdapContextSource();
|
||||
Assert.notEmpty(properties.getBaseDn(), "Base DN must be set with at least one value");
|
||||
source.setBase(properties.getBaseDn().get(0));
|
||||
source.setUrls(determineUrls(environment, properties.getPort()));
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
LdapTemplate ldapTemplate(LdapContextSource ldapContextSource) {
|
||||
return new LdapTemplate(ldapContextSource);
|
||||
}
|
||||
|
||||
private String[] determineUrls(Environment environment, int port) {
|
||||
return new String[] { "ldap://localhost:" + (port != 0 ? port : environment.getProperty("local.ldap.port")) };
|
||||
}
|
||||
|
||||
private void setSchema(InMemoryDirectoryServerConfig config) {
|
||||
config.setSchema(null);
|
||||
}
|
||||
|
||||
private void importLdif(ApplicationContext applicationContext) {
|
||||
String location = this.embeddedProperties.getLdif();
|
||||
if (StringUtils.hasText(location)) {
|
||||
try {
|
||||
Resource resource = applicationContext.getResource(location);
|
||||
if (resource.exists()) {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
this.server.importFromLDIF(true, new LDIFReader(inputStream));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to load LDIF " + location, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPortProperty(ApplicationContext context, int port) {
|
||||
if (context instanceof ConfigurableApplicationContext configurableContext) {
|
||||
MutablePropertySources sources = configurableContext.getEnvironment().getPropertySources();
|
||||
getLdapPorts(sources).put("local.ldap.port", port);
|
||||
}
|
||||
if (context.getParent() != null) {
|
||||
setPortProperty(context.getParent(), port);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> getLdapPorts(MutablePropertySources sources) {
|
||||
PropertySource<?> propertySource = sources.get(PROPERTY_SOURCE_NAME);
|
||||
if (propertySource == null) {
|
||||
propertySource = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>());
|
||||
sources.addFirst(propertySource);
|
||||
}
|
||||
return (Map<String, Object>) propertySource.getSource();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
if (this.server != null) {
|
||||
this.server.shutDown(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,6 @@ class SimpleLdapRepositoryTests {
|
||||
@BeforeEach
|
||||
void prepareTestedInstance() {
|
||||
tested = new SimpleLdapRepository<>(ldapOperationsMock, odmMock, Object.class);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2016-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.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.LdapRepository;
|
||||
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 {
|
||||
|
||||
LdapOperations ldapOperations = Mockito.mock();
|
||||
|
||||
ValueExpressionDelegate valueExpressionDelegate = ValueExpressionDelegate.create();
|
||||
|
||||
@Test
|
||||
void shouldEncodeQuery() throws NoSuchMethodException {
|
||||
LdapQueryMethod method = queryMethod("namedParameters");
|
||||
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
|
||||
|
||||
LdapQuery ldapQuery = query.createQuery(
|
||||
new LdapParametersParameterAccessor(method, new Object[] { "John)(cn=Doe)", "foo" }));
|
||||
|
||||
assertThat(ldapQuery.filter().encode()).isEqualTo("(cn=John\\29\\28cn=Doe\\29)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEncodeBase() throws NoSuchMethodException {
|
||||
LdapQueryMethod method = queryMethod("baseNamedParameters");
|
||||
AnnotatedLdapRepositoryQuery query = repositoryQuery(method);
|
||||
|
||||
LdapQuery ldapQuery = query.createQuery(
|
||||
new LdapParametersParameterAccessor(method, new Object[] { "foo", "cn=John)" }));
|
||||
|
||||
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),
|
||||
new DefaultRepositoryMetadata(QueryRepository.class), new SpelAwareProxyProjectionFactory());
|
||||
}
|
||||
|
||||
private AnnotatedLdapRepositoryQuery repositoryQuery(LdapQueryMethod method) {
|
||||
return new AnnotatedLdapRepositoryQuery(method, SchemaEntry.class, ldapOperations, new LdapMappingContext(),
|
||||
new EntityInstantiators(), valueExpressionDelegate);
|
||||
}
|
||||
|
||||
interface QueryRepository extends LdapRepository<SchemaEntry> {
|
||||
|
||||
@Query(value = "(cn=:fullName)")
|
||||
List<SchemaEntry> namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName);
|
||||
|
||||
@Query(base = ":dc", value = "(cn=:fullName)")
|
||||
List<SchemaEntry> baseNamedParameters(@Param("fullName") String fullName, @Param("dc") String dc);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2016-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 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;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })
|
||||
|
||||
public class SchemaEntry {
|
||||
@Id
|
||||
Name dn;
|
||||
|
||||
@Attribute(name = "cn")
|
||||
String fullName;
|
||||
|
||||
@Attribute
|
||||
String lastName;
|
||||
|
||||
public SchemaEntry() {}
|
||||
|
||||
public SchemaEntry(Name dn, String fullName, String lastName) {
|
||||
this.dn = dn;
|
||||
this.fullName = fullName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2016-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 static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.ldap.config.EmbeddedLdapProperties;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Integration tests for Repositories using value expressions.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@TestPropertySource(properties = { "full.name=John Doe", "dc.name=memorynotfound" })
|
||||
class ValueExpressionLdapRepositoryQueryTests {
|
||||
|
||||
@Autowired private QueryRepository queryRepository;
|
||||
|
||||
@Test
|
||||
void shouldWorkWithNamedParameters() {
|
||||
|
||||
List<SchemaEntry> objects = queryRepository.namedParameters("John Doe", "Bar");
|
||||
|
||||
assertThatReturnedObjectIsJohnDoe(objects);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingQueryLanguageCharsShouldNotFail() {
|
||||
|
||||
List<SchemaEntry> objects = queryRepository.namedParameters("John)(cn=Doe)", "Bar");
|
||||
|
||||
assertThat(objects).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithIndexParameters() {
|
||||
|
||||
List<SchemaEntry> objects = queryRepository.indexedParameters("John Doe", "Bar");
|
||||
|
||||
assertThatReturnedObjectIsJohnDoe(objects);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithSpelExpressions() {
|
||||
|
||||
List<SchemaEntry> objects = queryRepository.spelParameters();
|
||||
|
||||
assertThatReturnedObjectIsJohnDoe(objects);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithPropertyPlaceholders() {
|
||||
|
||||
List<SchemaEntry> objects = queryRepository.propertyPlaceholderParameters();
|
||||
|
||||
assertThatReturnedObjectIsJohnDoe(objects);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWorkWithNamedParametersForBase() {
|
||||
|
||||
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();
|
||||
|
||||
assertThatReturnedObjectIsJohnDoe(objects);
|
||||
}
|
||||
|
||||
private static void assertThatReturnedObjectIsJohnDoe(List<SchemaEntry> objects) {
|
||||
|
||||
assertThat(objects).hasSize(1);
|
||||
assertThat(objects.get(0).fullName).isEqualTo("John Doe");
|
||||
assertThat(objects.get(0).lastName).isEqualTo("Doe");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import(InMemoryLdapConfiguration.class)
|
||||
@EnableLdapRepositories(considerNestedRepositories = true)
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
EmbeddedLdapProperties embeddedLdapProperties() {
|
||||
EmbeddedLdapProperties embeddedLdapProperties = new EmbeddedLdapProperties();
|
||||
embeddedLdapProperties.setBaseDn(Arrays.asList("dc=com", "dc=memorynotfound"));
|
||||
return embeddedLdapProperties;
|
||||
}
|
||||
}
|
||||
|
||||
interface QueryRepository extends LdapRepository<SchemaEntry> {
|
||||
|
||||
@Query(value = "(cn=:fullName)")
|
||||
List<SchemaEntry> namedParameters(@Param("fullName") String fullName, @Param("lastName") String lastName);
|
||||
|
||||
@Query(value = "(cn=?0)")
|
||||
List<SchemaEntry> indexedParameters(String fullName, String lastName);
|
||||
|
||||
@Query(value = "(cn=:#{'John ' + 'Doe'})")
|
||||
List<SchemaEntry> spelParameters();
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
71
src/test/resources/schema.ldif
Normal file
71
src/test/resources/schema.ldif
Normal file
@@ -0,0 +1,71 @@
|
||||
dn: dc=com
|
||||
objectclass: top
|
||||
objectclass: domain
|
||||
dc: com
|
||||
|
||||
dn: dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: domain
|
||||
objectclass: extensibleObject
|
||||
dc: memorynotfound
|
||||
|
||||
# Organizational Units
|
||||
dn: ou=groups,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: ou=people,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
# Create People
|
||||
dn: uid=john,ou=people,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: person
|
||||
objectclass: organizationalPerson
|
||||
objectclass: inetOrgPerson
|
||||
cn: John Doe
|
||||
sn: John
|
||||
uid: john
|
||||
fullName: John Doe
|
||||
lastName: Doe
|
||||
|
||||
dn: uid=jihn,ou=people,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: person
|
||||
objectclass: organizationalPerson
|
||||
objectclass: inetOrgPerson
|
||||
cn: Jihn Die
|
||||
sn: Jihn
|
||||
uid: jihn
|
||||
fullName: Jihn Die
|
||||
lastName: Die
|
||||
|
||||
dn: uid=jahn,ou=people,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: person
|
||||
objectclass: organizationalPerson
|
||||
objectclass: inetOrgPerson
|
||||
cn: Jahn Dae
|
||||
sn: Jahn
|
||||
uid: jahn
|
||||
fullName: Jahn Die
|
||||
lastName: Dae
|
||||
|
||||
# Create Groups
|
||||
dn: cn=developers,ou=groups,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: groupOfUniqueNames
|
||||
cn: developers
|
||||
ou: developer
|
||||
uniqueMember: uid=john,ou=people,dc=memorynotfound,dc=com
|
||||
uniqueMember: uid=jihn,ou=people,dc=memorynotfound,dc=com
|
||||
|
||||
dn: cn=managers,ou=groups,dc=memorynotfound,dc=com
|
||||
objectclass: top
|
||||
objectclass: groupOfUniqueNames
|
||||
cn: managers
|
||||
ou: manager
|
||||
uniqueMember: uid=jahn,ou=people,dc=memorynotfound,dc=com
|
||||
Reference in New Issue
Block a user