diff --git a/pom.xml b/pom.xml
index a19cb3a82..310678df8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -211,7 +211,7 @@
org.hsqldbhsqldb
- 2.2.8
+ 2.4.1test
diff --git a/src/main/asciidoc/jpa.adoc b/src/main/asciidoc/jpa.adoc
index a3ae0d9e7..d05d4de51 100644
--- a/src/main/asciidoc/jpa.adoc
+++ b/src/main/asciidoc/jpa.adoc
@@ -157,6 +157,11 @@ This section describes the various ways to create a query with Spring Data JPA.
The JPA module supports defining a query manually as a String or having it being derived from the method name.
+Derived queries with the predicates `IsStartingWith`, `StartingWith`, `StartsWith`, IsEndingWith", `EndingWith`, `EndsWith`,
+`IsNotContaining`, `NotContaining`, `NotContains`, `IsContaining`, `Containing`, `Contains` the respective arguments for these queries will get sanitized.
+This means if the arguments actually contain characters recognized by `LIKE` as wildcards these will get escaped so they match only as literals.
+Compare with <>.
+
==== Declared Queries
Although getting a query derived from the method name is quite convenient, one might face the situation in which either the method name parser does not support the keyword one wants to use or the method name would get unnecessarily ugly. So you can either use JPA named queries through a naming convention (see <> for more information) or rather annotate your query method with `@Query` (see <> for details).
@@ -461,6 +466,51 @@ public interface ConcreteRepository
In the preceding example, the `MappedTypeRepository` interface is the common parent interface for a few domain types extending `AbstractMappedType`. It also defines the generic `findAllByAttribute(…)` method, which can be used on instances of the specialized repository interfaces. If you now invoke `findByAllAttribute(…)` on `ConcreteRepository`, the query becomes `select t from ConcreteType t where t.attribute = ?1`.
+SpEL expressions to manipulate arguments may also be used to manipulate method arguments.
+In these SpEL expressions the entity name is not available, but the arguments are.
+They can be accessed by name or index as demonstrated in the following example.
+
+.Using SpEL expressions in repository query methods - accessing arguments.
+====
+[source, java]
+----
+@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
+List findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
+----
+====
+
+For `like`-conditions one often wants to appen `%` to the beginning or the end of a String valued parameter.
+This can be done by appending or prefixing a bind parameter marker or a SpEL expression with `%`.
+Again the following example demonstrates this.
+
+.Using SpEL expressions in repository query methods - wildcard shortcut.
+====
+[source, java]
+----
+@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
+List findByLastnameWithSpelExpression(@Param("lastname") String lastname);
+----
+====
+
+When using `like`-conditions with values that are coming from a not secure source the values should be sanitized so they can't contain any wildcards and thereby allow attackers to select more data than they should be able to.
+For this purpose the the `escape(String, String)` method is made available in the SpEL context.
+It prefixes all instances of `_` and `%` in the first argument with the single character from the second argument.
+In combination with the `escape` clause of the `like` expression available in JPQL and standard SQL this allows easy cleaning of bind parameters.
+
+
+.Using SpEL expressions in repository query methods - sanitizing input values.
+====
+[source, java]
+----
+@Query("select u from User u where u.firstname like %?#{#escape([0],'#')}% escape '#'")
+List findContainingEscaped(String namePart);
+----
+====
+
+Given this method declaration in an repository interface `findContainingEscaped("Peter_")" will find `Peter_Parker` but not `Peter Parker`.
+Note that the method `escape(String, String)` available in the SpEL context will only escape the SQL and JPQL standard wildcards `_` and `%`.
+If the underlying database or the JPA implementation supports additional wildcards these will not get escaped.
+
[[jpa.modifying-queries]]
=== Modifying Queries
diff --git a/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java b/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
index 2add3039c..365563970 100644
--- a/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
+++ b/src/main/java/org/springframework/data/jpa/repository/config/EnableJpaRepositories.java
@@ -166,4 +166,12 @@ public @interface EnableJpaRepositories {
* @since 2.1
*/
BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT;
+
+ /**
+ * Configures what character is used to escape the wildcards {@literal _} and {@literal %} in derived queries with
+ * {@literal contains}, {@literal startsWith} or {@literal endsWith} clauses.
+ *
+ * @return a single character used for escaping.
+ */
+ char escapeCharacter() default '\\';
}
diff --git a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
index e9f268278..1f484a738 100644
--- a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
+++ b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
@@ -132,9 +132,22 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
Optional transactionManagerRef = source.getAttribute("transactionManagerRef");
builder.addPropertyValue("transactionManager", transactionManagerRef.orElse(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME));
builder.addPropertyValue("entityManager", getEntityManagerBeanDefinitionFor(source, source.getSource()));
+ builder.addPropertyValue("escapeCharacter", getEscapeCharacter(source).orElse('\\'));
builder.addPropertyReference("mappingContext", JPA_MAPPING_CONTEXT_BEAN_NAME);
}
+ /**
+ * XML configurations do not support {@link Character} values. This method catches the exception thrown and returns an {@link Optional#empty()} instead.
+ */
+ private static Optional getEscapeCharacter(RepositoryConfigurationSource source) {
+
+ try {
+ return source.getAttribute("escapeCharacter", Character.class);
+ } catch (IllegalArgumentException ___) {
+ return Optional.empty();
+ }
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource)
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
index 3877f31aa..1e5955fce 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
@@ -36,6 +36,7 @@ import javax.persistence.metamodel.SingularAttribute;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
+import org.springframework.data.jpa.repository.support.EscapeCharacter;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -62,6 +63,7 @@ public class JpaQueryCreator extends AbstractQueryCreator criteriaQuery = createCriteriaQuery(builder, type);
+ CriteriaQuery> criteriaQuery = createCriteriaQuery(builder, type);
this.builder = builder;
this.query = criteriaQuery.distinct(tree.isDistinct());
this.root = query.from(type.getDomainType());
this.provider = provider;
this.returnedType = type;
+ this.escape = provider.getEscape();
}
/**
@@ -289,7 +292,7 @@ public class JpaQueryCreator extends AbstractQueryCreator stringPath = getTypedPath(root, part);
Expression propertyExpression = upperIfIgnoreCase(stringPath);
Expression parameterExpression = upperIfIgnoreCase(provider.next(part, String.class).getExpression());
- Predicate like = builder.like(propertyExpression, parameterExpression);
+ Predicate like = builder.like(propertyExpression, parameterExpression, escape.getValue());
return type.equals(NOT_LIKE) || type.equals(NOT_CONTAINING) ? like.not() : like;
case TRUE:
Expression truePath = getTypedPath(root, part);
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
index 504041c9b..78909bf36 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java
@@ -22,6 +22,7 @@ import javax.persistence.EntityManager;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.support.EscapeCharacter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -91,16 +92,19 @@ public final class JpaQueryLookupStrategy {
private static class CreateQueryLookupStrategy extends AbstractQueryLookupStrategy {
private final PersistenceProvider persistenceProvider;
+ private final EscapeCharacter escape;
- public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor) {
+ public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escape) {
super(em, extractor);
+
this.persistenceProvider = PersistenceProvider.fromEntityManager(em);
+ this.escape = escape;
}
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
- return new PartTreeJpaQuery(method, em, persistenceProvider);
+ return new PartTreeJpaQuery(method, em, persistenceProvider, escape);
}
}
@@ -217,10 +221,11 @@ public final class JpaQueryLookupStrategy {
* @param key may be {@literal null}.
* @param extractor must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
+ * @param escape
* @return
*/
public static QueryLookupStrategy create(EntityManager em, @Nullable Key key, QueryExtractor extractor,
- QueryMethodEvaluationContextProvider evaluationContextProvider) {
+ QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escape) {
Assert.notNull(em, "EntityManager must not be null!");
Assert.notNull(extractor, "QueryExtractor must not be null!");
@@ -228,11 +233,12 @@ public final class JpaQueryLookupStrategy {
switch (key != null ? key : Key.CREATE_IF_NOT_FOUND) {
case CREATE:
- return new CreateQueryLookupStrategy(em, extractor);
+ return new CreateQueryLookupStrategy(em, extractor, escape);
case USE_DECLARED_QUERY:
return new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider);
case CREATE_IF_NOT_FOUND:
- return new CreateIfNotFoundQueryLookupStrategy(em, extractor, new CreateQueryLookupStrategy(em, extractor),
+ return new CreateIfNotFoundQueryLookupStrategy(em, extractor,
+ new CreateQueryLookupStrategy(em, extractor, escape),
new DeclaredQueryLookupStrategy(em, extractor, evaluationContextProvider));
default:
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java
index 0fc4ac3b8..bd0607282 100644
--- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java
+++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterMetadataProvider.java
@@ -27,6 +27,7 @@ import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.ParameterExpression;
import org.springframework.data.jpa.provider.PersistenceProvider;
+import org.springframework.data.jpa.repository.support.EscapeCharacter;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
@@ -54,31 +55,32 @@ class ParameterMetadataProvider {
private final List> expressions;
private final @Nullable Iterator