diff --git a/pom.xml b/pom.xml
index cd39cda..9661760 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-ldap
- 3.5.0-SNAPSHOT
+ 3.5.0-GH-509-SNAPSHOTSpring Data LDAPSpring Data integration for LDAP
diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc
index edb3d96..5f13198 100644
--- a/src/main/antora/modules/ROOT/nav.adoc
+++ b/src/main/antora/modules/ROOT/nav.adoc
@@ -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[]
diff --git a/src/main/antora/modules/ROOT/pages/ldap/query-methods.adoc b/src/main/antora/modules/ROOT/pages/ldap/query-methods.adoc
index bdc318e..9d9c29d 100644
--- a/src/main/antora/modules/ROOT/pages/ldap/query-methods.adoc
+++ b/src/main/antora/modules/ROOT/pages/ldap/query-methods.adoc
@@ -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 {
+
+ @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 {
+
+ @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 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 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.
diff --git a/src/main/antora/modules/ROOT/pages/ldap/value-expressions.adoc b/src/main/antora/modules/ROOT/pages/ldap/value-expressions.adoc
new file mode 100644
index 0000000..6356a46
--- /dev/null
+++ b/src/main/antora/modules/ROOT/pages/ldap/value-expressions.adoc
@@ -0,0 +1 @@
+include::{commons}@data-commons::page$value-expressions.adoc[]
diff --git a/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java b/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java
new file mode 100644
index 0000000..2c01f39
--- /dev/null
+++ b/src/main/java/org/springframework/data/ldap/repository/LdapEncode.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * Annotation which indicates that a method parameter should be encoded using a specific {@link LdapEncoder} for a
+ * repository query method invocation.
+ *
+ * If no {@link LdapEncoder} is configured, method parameters are encoded using
+ * {@link org.springframework.ldap.support.LdapEncoder#filterEncode(String)}. The default encoder considers chars such
+ * as {@code *} (asterisk) to be encoded which might interfere with the intent of running a Like query. Since Spring
+ * Data LDAP doesn't parse queries it is up to you to decide which encoder to use.
+ *
+ * {@link LdapEncoder} implementations must declare a no-args constructor so they can be instantiated during repository
+ * initialization.
+ *
+ * @author Marcin Grzejszczak
+ * @author Mark Paluch
+ * @since 3.5
+ */
+@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface LdapEncode {
+
+ /**
+ * {@link LdapEncoder} to encode query parameters.
+ *
+ * @return {@link LdapEncoder} class
+ */
+ @AliasFor("encoder")
+ Class extends LdapEncoder> value();
+
+ /**
+ * {@link LdapEncoder} to encode query parameters.
+ *
+ * @return {@link LdapEncoder} class
+ */
+ @AliasFor("value")
+ Class extends LdapEncoder> encoder() default LdapEncoder.class;
+
+}
diff --git a/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java b/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java
new file mode 100644
index 0000000..02dbba1
--- /dev/null
+++ b/src/main/java/org/springframework/data/ldap/repository/LdapEncoder.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * Strategy interface to escape values for use in LDAP filters.
+ *
+ * Accepts an LDAP filter value to be encoded (escaped) for String-based LDAP query usage as LDAP queries do not feature
+ * an out-of-band parameter binding mechanism.
+ *
+ * Make sure that your implementation escapes special characters in the value adequately to prevent injection attacks.
+ *
+ * @author Marcin Grzejszczak
+ * @author Mark Paluch
+ * @since 3.5
+ */
+public interface LdapEncoder {
+
+ /**
+ * Encode a value for use in a filter.
+ *
+ * @param value the value to encode.
+ * @return a properly encoded representation of the supplied value.
+ */
+ String encode(String value);
+
+}
diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java
index d35e6fe..4c4d1f9 100644
--- a/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java
+++ b/src/main/java/org/springframework/data/ldap/repository/query/AbstractLdapRepositoryQuery.java
@@ -26,7 +26,6 @@
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;
diff --git a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java
index 708fc9f..a450a90 100644
--- a/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java
+++ b/src/main/java/org/springframework/data/ldap/repository/query/AnnotatedLdapRepositoryQuery.java
@@ -39,9 +39,9 @@
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.
@@ -81,34 +81,32 @@ public AnnotatedLdapRepositoryQuery(LdapQueryMethod queryMethod, Class> entity
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));
}
}
diff --git a/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java b/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java
deleted file mode 100644
index 2782acc..0000000
--- a/src/main/java/org/springframework/data/ldap/repository/query/BindingContext.java
+++ /dev/null
@@ -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 bindings;
-
- private final ValueExpressionEvaluator evaluator;
-
- /**
- * Create new {@link BindingContext}.
- */
- BindingContext(Parameters, ?> parameters, ParameterAccessor parameterAccessor,
- List 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