DATAJPA-1519 - Wildcard escaping in LIKE-queries.
Derived queries with `contains`, `startWith` or `endsWith` predicates now use wildcard escaping in order to avoid selecting more then requested. A function `escape(String, String)` is made available in SpEL expressions as well for the same purpose. Note that annotated queries and derived queries with LIKE conditions don't get escaped because it is not clear if the use of wildcards in the argument is intended or not.
This commit is contained in:
committed by
Oliver Drotbohm
parent
cde0b1dacf
commit
9b16fef6e9
2
pom.xml
2
pom.xml
@@ -211,7 +211,7 @@
|
||||
<dependency>
|
||||
<groupId>org.hsqldb</groupId>
|
||||
<artifactId>hsqldb</artifactId>
|
||||
<version>2.2.8</version>
|
||||
<version>2.4.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -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 <<jpa.query.spel-expressions>>.
|
||||
|
||||
==== 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 <<jpa.query-methods.named-queries>> for more information) or rather annotate your query method with `@Query` (see <<jpa.query-methods.at-query>> 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<User> 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<User> 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<User> 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
|
||||
|
||||
|
||||
@@ -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 '\\';
|
||||
}
|
||||
|
||||
@@ -132,9 +132,22 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
|
||||
Optional<String> 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<Character> 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)
|
||||
|
||||
@@ -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<? extend
|
||||
private final ParameterMetadataProvider provider;
|
||||
private final ReturnedType returnedType;
|
||||
private final PartTree tree;
|
||||
private final EscapeCharacter escape;
|
||||
|
||||
/**
|
||||
* Create a new {@link JpaQueryCreator}.
|
||||
@@ -77,13 +79,14 @@ public class JpaQueryCreator extends AbstractQueryCreator<CriteriaQuery<? extend
|
||||
super(tree);
|
||||
this.tree = tree;
|
||||
|
||||
CriteriaQuery<? extends Object> 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<CriteriaQuery<? extend
|
||||
Expression<String> stringPath = getTypedPath(root, part);
|
||||
Expression<String> propertyExpression = upperIfIgnoreCase(stringPath);
|
||||
Expression<String> 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<Boolean> truePath = getTypedPath(root, part);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<ParameterMetadata<?>> expressions;
|
||||
private final @Nullable Iterator<Object> bindableParameterValues;
|
||||
private final PersistenceProvider persistenceProvider;
|
||||
private final EscapeCharacter escape;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and
|
||||
* {@link ParametersParameterAccessor} with support for parameter value customizations via {@link PersistenceProvider}
|
||||
* .
|
||||
*
|
||||
* @param builder must not be {@literal null}.
|
||||
* @param builder must not be {@literal null}.
|
||||
* @param accessor must not be {@literal null}.
|
||||
* @param provider must not be {@literal null}.
|
||||
* @param escape
|
||||
*/
|
||||
public ParameterMetadataProvider(CriteriaBuilder builder, ParametersParameterAccessor accessor,
|
||||
PersistenceProvider provider) {
|
||||
this(builder, accessor.iterator(), accessor.getParameters(), provider);
|
||||
PersistenceProvider provider, EscapeCharacter escape) {
|
||||
this(builder, accessor.iterator(), accessor.getParameters(), provider, escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with
|
||||
* support for parameter value customizations via {@link PersistenceProvider}.
|
||||
*
|
||||
* @param builder must not be {@literal null}.
|
||||
* @param builder must not be {@literal null}.
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param provider must not be {@literal null}.
|
||||
* @param escape
|
||||
*/
|
||||
public ParameterMetadataProvider(CriteriaBuilder builder, Parameters<?, ?> parameters, PersistenceProvider provider) {
|
||||
this(builder, null, parameters, provider);
|
||||
public ParameterMetadataProvider(CriteriaBuilder builder, Parameters<?, ?> parameters, PersistenceProvider provider, EscapeCharacter escape) {
|
||||
this(builder, null, parameters, provider, escape);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,9 +92,10 @@ class ParameterMetadataProvider {
|
||||
* @param bindableParameterValues may be {@literal null}.
|
||||
* @param parameters must not be {@literal null}.
|
||||
* @param provider must not be {@literal null}.
|
||||
* @param escape
|
||||
*/
|
||||
private ParameterMetadataProvider(CriteriaBuilder builder, @Nullable Iterator<Object> bindableParameterValues,
|
||||
Parameters<?, ?> parameters, PersistenceProvider provider) {
|
||||
Parameters<?, ?> parameters, PersistenceProvider provider, EscapeCharacter escape) {
|
||||
|
||||
Assert.notNull(builder, "CriteriaBuilder must not be null!");
|
||||
Assert.notNull(parameters, "Parameters must not be null!");
|
||||
@@ -103,6 +106,7 @@ class ParameterMetadataProvider {
|
||||
this.expressions = new ArrayList<>();
|
||||
this.bindableParameterValues = bindableParameterValues;
|
||||
this.persistenceProvider = provider;
|
||||
this.escape = escape;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,12 +174,16 @@ class ParameterMetadataProvider {
|
||||
|
||||
Object value = bindableParameterValues == null ? ParameterMetadata.PLACEHOLDER : bindableParameterValues.next();
|
||||
|
||||
ParameterMetadata<T> metadata = new ParameterMetadata<>(expression, part.getType(), value, persistenceProvider);
|
||||
ParameterMetadata<T> metadata = new ParameterMetadata<>(expression, part.getType(), value, persistenceProvider, escape);
|
||||
expressions.add(metadata);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
EscapeCharacter getEscape() {
|
||||
return escape;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
@@ -188,16 +196,18 @@ class ParameterMetadataProvider {
|
||||
private final Type type;
|
||||
private final ParameterExpression<T> expression;
|
||||
private final PersistenceProvider persistenceProvider;
|
||||
private final EscapeCharacter escape;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParameterMetadata}.
|
||||
*/
|
||||
public ParameterMetadata(ParameterExpression<T> expression, Type type, @Nullable Object value,
|
||||
PersistenceProvider provider) {
|
||||
PersistenceProvider provider, EscapeCharacter escape) {
|
||||
|
||||
this.expression = expression;
|
||||
this.persistenceProvider = provider;
|
||||
this.type = value == null && Type.SIMPLE_PROPERTY.equals(type) ? Type.IS_NULL : type;
|
||||
this.escape = escape;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,12 +242,12 @@ class ParameterMetadataProvider {
|
||||
|
||||
switch (type) {
|
||||
case STARTING_WITH:
|
||||
return String.format("%s%%", value.toString());
|
||||
return String.format("%s%%", escape.escape(value.toString()));
|
||||
case ENDING_WITH:
|
||||
return String.format("%%%s", value.toString());
|
||||
return String.format("%%%s", escape.escape(value.toString()));
|
||||
case CONTAINING:
|
||||
case NOT_CONTAINING:
|
||||
return String.format("%%%s%%", value.toString());
|
||||
return String.format("%%%s%%", escape.escape(value.toString()));
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.query.JpaQueryExecution.DeleteExecution;
|
||||
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ExistsExecution;
|
||||
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.ResultProcessor;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
@@ -53,19 +54,21 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
private final QueryPreparer query;
|
||||
private final QueryPreparer countQuery;
|
||||
private final EntityManager em;
|
||||
private final EscapeCharacter escape;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PartTreeJpaQuery}.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @param em must not be {@literal null}.
|
||||
* @param persistenceProvider must not be {@literal null}.
|
||||
* @param escape
|
||||
*/
|
||||
PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider) {
|
||||
PartTreeJpaQuery(JpaQueryMethod method, EntityManager em, PersistenceProvider persistenceProvider, EscapeCharacter escape) {
|
||||
|
||||
super(method, em);
|
||||
|
||||
this.em = em;
|
||||
this.escape = escape;
|
||||
Class<?> domainClass = method.getEntityInformation().getJavaType();
|
||||
this.parameters = method.getParameters();
|
||||
|
||||
@@ -226,8 +229,8 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
|
||||
|
||||
ParameterMetadataProvider provider = accessor
|
||||
.map(it -> new ParameterMetadataProvider(builder, it, persistenceProvider))//
|
||||
.orElseGet(() -> new ParameterMetadataProvider(builder, parameters, persistenceProvider));
|
||||
.map(it -> new ParameterMetadataProvider(builder, it, persistenceProvider, escape))//
|
||||
.orElseGet(() -> new ParameterMetadataProvider(builder, parameters, persistenceProvider, escape));
|
||||
|
||||
ResultProcessor processor = getQueryMethod().getResultProcessor();
|
||||
ReturnedType returnedType = accessor.map(processor::withDynamicProjection)//
|
||||
@@ -280,8 +283,8 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
|
||||
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
|
||||
|
||||
ParameterMetadataProvider provider = accessor
|
||||
.map(it -> new ParameterMetadataProvider(builder, it, persistenceProvider))//
|
||||
.orElseGet(() -> new ParameterMetadataProvider(builder, parameters, persistenceProvider));
|
||||
.map(it -> new ParameterMetadataProvider(builder, it, persistenceProvider, escape))//
|
||||
.orElseGet(() -> new ParameterMetadataProvider(builder, parameters, persistenceProvider, escape));
|
||||
|
||||
return new JpaCountQueryCreator(tree, getQueryMethod().getResultProcessor().getReturnedType(), builder, provider);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -25,6 +26,7 @@ import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
|
||||
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
|
||||
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
|
||||
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
|
||||
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.QueryMethodEvaluationContextProvider;
|
||||
@@ -34,6 +36,7 @@ import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Encapsulates different strategies for the creation of a {@link QueryParameterSetter} from a {@link Query} and a
|
||||
@@ -175,6 +178,11 @@ abstract class QueryParameterSetterFactory {
|
||||
private Object evaluateExpression(Expression expression, Object[] values) {
|
||||
|
||||
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameters, values);
|
||||
Method escapeMethod = ReflectionUtils.findMethod(EscapeCharacter.class, "escape", String.class, String.class);
|
||||
|
||||
Assert.notNull(escapeMethod, "Escape method must not be null.");
|
||||
|
||||
context.setVariable("escape", escapeMethod);
|
||||
return expression.getValue(context, Object.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 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
|
||||
*
|
||||
* http://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.jpa.repository.support;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A Value-class encapsulating an escape character for LIKE queries and the actually usage of it in escaping Strings.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@Value(staticConstructor = "of")
|
||||
public class EscapeCharacter {
|
||||
char value;
|
||||
|
||||
public String escape(String value) {
|
||||
|
||||
Assert.notNull(value, "Value must be not null.");
|
||||
|
||||
return value.replace("_", value + "_").replace("%", value + "%");
|
||||
}
|
||||
|
||||
// used for SpEL expressions
|
||||
static String escape(String value, String escape) {
|
||||
|
||||
Assert.hasText(escape, "escape must be a sinlge character String.");
|
||||
char[] chars = escape.toCharArray();
|
||||
Assert.isTrue(chars.length == 1, "escape must be a single character String.");
|
||||
|
||||
return EscapeCharacter.of(chars[0]).escape(value);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
|
||||
private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor;
|
||||
|
||||
private EntityPathResolver entityPathResolver;
|
||||
private EscapeCharacter escapeCharacter = EscapeCharacter.of('\\');
|
||||
|
||||
/**
|
||||
* Creates a new {@link JpaRepositoryFactory}.
|
||||
@@ -113,6 +114,15 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
|
||||
this.entityPathResolver = entityPathResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the escape character to be used for like-expressions created for derived queries.
|
||||
*
|
||||
* @param escapeCharacter a character used for escaping in certain like expressions.
|
||||
*/
|
||||
public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
|
||||
this.escapeCharacter = escapeCharacter;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryMetadata)
|
||||
@@ -174,7 +184,8 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
|
||||
@Override
|
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
return Optional.of(JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider));
|
||||
return Optional
|
||||
.of(JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider, escapeCharacter));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -44,6 +44,7 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
|
||||
|
||||
private @Nullable EntityManager entityManager;
|
||||
private EntityPathResolver entityPathResolver;
|
||||
private EscapeCharacter escapeCharacter = EscapeCharacter.of('\\');
|
||||
|
||||
/**
|
||||
* Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface.
|
||||
@@ -103,7 +104,7 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
|
||||
|
||||
JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
|
||||
jpaRepositoryFactory.setEntityPathResolver(entityPathResolver);
|
||||
|
||||
jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);
|
||||
return jpaRepositoryFactory;
|
||||
}
|
||||
|
||||
@@ -118,4 +119,9 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
|
||||
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
|
||||
public void setEscapeCharacter(char escapeCharacter) {
|
||||
|
||||
this.escapeCharacter = EscapeCharacter.of(escapeCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,16 @@ public class UserRepositoryFinderTests {
|
||||
assertThat(userRepository.findByLastnameNotContaining("u"), containsInAnyOrder(dave, oliver));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-1519
|
||||
public void parametersForContainsGetProperlyEscaped() {
|
||||
assertThat(userRepository.findByFirstnameContaining("liv%"), iterableWithSize(0));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-1519
|
||||
public void escapingInLikeSpels() {
|
||||
assertThat(userRepository.findContainingEscaped("att_"), iterableWithSize(0));
|
||||
}
|
||||
|
||||
@Test // DATAJPA-829
|
||||
public void translatesContainsToMemberOf() {
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.data.jpa.domain.sample.Role;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.provider.HibernateUtils;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
|
||||
@@ -60,7 +61,7 @@ public class JpaCountQueryCreatorIntegrationTests {
|
||||
|
||||
PartTree tree = new PartTree("findDistinctByRolesIn", User.class);
|
||||
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(entityManager.getCriteriaBuilder(),
|
||||
queryMethod.getParameters(), provider);
|
||||
queryMethod.getParameters(), provider, EscapeCharacter.of('\\'));
|
||||
|
||||
JpaCountQueryCreator creator = new JpaCountQueryCreator(tree, queryMethod.getResultProcessor().getReturnedType(),
|
||||
entityManager.getCriteriaBuilder(), metadataProvider);
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
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.Repository;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
@@ -76,7 +77,7 @@ public class JpaQueryLookupStrategyUnitTests {
|
||||
public void invalidAnnotatedQueryCausesException() throws Exception {
|
||||
|
||||
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor,
|
||||
EVALUATION_CONTEXT_PROVIDER);
|
||||
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.of('\\'));
|
||||
Method method = UserRepository.class.getMethod("findByFoo", String.class);
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
|
||||
|
||||
@@ -92,7 +93,7 @@ public class JpaQueryLookupStrategyUnitTests {
|
||||
public void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception {
|
||||
|
||||
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor,
|
||||
EVALUATION_CONTEXT_PROVIDER);
|
||||
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.of('\\'));
|
||||
Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Sort.class);
|
||||
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.repository.query.DefaultParameters;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
@@ -58,7 +59,7 @@ public class ParameterExpressionProviderTests {
|
||||
|
||||
CriteriaBuilder builder = em.getCriteriaBuilder();
|
||||
PersistenceProvider persistenceProvider = PersistenceProvider.fromEntityManager(em);
|
||||
ParameterMetadataProvider provider = new ParameterMetadataProvider(builder, accessor, persistenceProvider);
|
||||
ParameterMetadataProvider provider = new ParameterMetadataProvider(builder, accessor, persistenceProvider, EscapeCharacter.of('\\'));
|
||||
ParameterExpression<? extends Comparable> expression = provider.next(part, Comparable.class).getExpression();
|
||||
assertThat(expression.getParameterType(), is(typeCompatibleWith(int.class)));
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
@@ -28,6 +28,7 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
@@ -81,7 +82,7 @@ public class ParameterMetadataProviderIntegrationTests {
|
||||
simulateDiscoveredParametername(parameters);
|
||||
|
||||
return new ParameterMetadataProvider(em.getCriteriaBuilder(), parameters,
|
||||
PersistenceProvider.fromEntityManager(em));
|
||||
PersistenceProvider.fromEntityManager(em), EscapeCharacter.of('\\'));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -24,6 +24,7 @@ import javax.persistence.criteria.CriteriaBuilder;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
|
||||
@@ -44,7 +45,7 @@ public class ParameterMetadataProviderUnitTests {
|
||||
when(parameters.getBindableParameters().iterator()).thenReturn(Collections.emptyListIterator());
|
||||
|
||||
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(builder, parameters,
|
||||
persistenceProvider);
|
||||
persistenceProvider, EscapeCharacter.of('\\'));
|
||||
|
||||
assertThatExceptionOfType(RuntimeException.class) //
|
||||
.isThrownBy(() -> metadataProvider.next(mock(Part.class))) //
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.springframework.data.jpa.domain.sample.User;
|
||||
import org.springframework.data.jpa.provider.HibernateUtils;
|
||||
import org.springframework.data.jpa.provider.PersistenceProvider;
|
||||
import org.springframework.data.jpa.repository.Temporal;
|
||||
import org.springframework.data.jpa.repository.support.EscapeCharacter;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
@@ -81,7 +82,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void test() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("findByFirstname", String.class, Pageable.class);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
jpaQuery.createQuery(new Object[] { "Matthews", PageRequest.of(0, 1) });
|
||||
jpaQuery.createQuery(new Object[] { "Matthews", PageRequest.of(0, 1) });
|
||||
@@ -105,7 +106,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void recreatesQueryIfNullValueIsGiven() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("findByFirstname", String.class, Pageable.class);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
Query query = jpaQuery.createQuery(new Object[] { "Matthews", PageRequest.of(0, 1) });
|
||||
|
||||
@@ -120,7 +121,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void shouldLimitExistsProjectionQueries() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("existsByFirstname", String.class);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
Query query = jpaQuery.createQuery(new Object[] { "Matthews" });
|
||||
|
||||
@@ -131,7 +132,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void shouldSelectAliasedIdForExistsProjectionQueries() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("existsByFirstname", String.class);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
Query query = jpaQuery.createQuery(new Object[] { "Matthews" });
|
||||
|
||||
@@ -142,7 +143,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void isEmptyCollection() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("findByRolesIsEmpty");
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
Query query = jpaQuery.createQuery(new Object[] {});
|
||||
|
||||
@@ -153,7 +154,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void isNotEmptyCollection() throws Exception {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod("findByRolesIsNotEmpty");
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
Query query = jpaQuery.createQuery(new Object[] {});
|
||||
|
||||
@@ -164,7 +165,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
public void rejectsIsEmptyOnNonCollectionProperty() throws Exception {
|
||||
|
||||
JpaQueryMethod method = getQueryMethod("findByFirstnameIsEmpty");
|
||||
AbstractJpaQuery jpaQuery = new PartTreeJpaQuery(method, entityManager, provider);
|
||||
AbstractJpaQuery jpaQuery = new PartTreeJpaQuery(method, entityManager, provider, EscapeCharacter.of('\\'));
|
||||
|
||||
jpaQuery.createQuery(new Object[] { "Oliver" });
|
||||
}
|
||||
@@ -175,7 +176,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
JpaQueryMethod method = getQueryMethod("findByFirstname");
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> new PartTreeJpaQuery(method, entityManager, provider)) //
|
||||
.isThrownBy(() -> new PartTreeJpaQuery(method, entityManager, provider, EscapeCharacter.of('\\'))) //
|
||||
.withMessageContaining("findByFirstname") // the method being analyzed
|
||||
.withMessageContaining(" firstname ") // the property we are looking for
|
||||
.withMessageContaining("UserRepository"); // the repository
|
||||
@@ -187,7 +188,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
JpaQueryMethod method = getQueryMethod("findByNoSuchProperty", String.class);
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class) //
|
||||
.isThrownBy(() -> new PartTreeJpaQuery(method, entityManager, provider)) //
|
||||
.isThrownBy(() -> new PartTreeJpaQuery(method, entityManager, provider, EscapeCharacter.of('\\'))) //
|
||||
.withMessageContaining("findByNoSuchProperty") // the method being analyzed
|
||||
.withMessageContaining(" noSuchProperty ") // the property we are looking for
|
||||
.withMessageContaining("UserRepository"); // the repository
|
||||
@@ -203,7 +204,7 @@ public class PartTreeJpaQueryIntegrationTests {
|
||||
|
||||
JpaQueryMethod queryMethod = getQueryMethod(methodName, parameterTypes);
|
||||
PartTreeJpaQuery jpaQuery = new PartTreeJpaQuery(queryMethod, entityManager,
|
||||
PersistenceProvider.fromEntityManager(entityManager));
|
||||
PersistenceProvider.fromEntityManager(entityManager), EscapeCharacter.of('\\'));
|
||||
jpaQuery.createQuery(values);
|
||||
}
|
||||
|
||||
|
||||
@@ -558,6 +558,10 @@ public interface UserRepository
|
||||
// DATAJPA-1334
|
||||
List<NameOnlyDto> findByNamedQueryWithConstructorExpression();
|
||||
|
||||
// DATAJPA-1519
|
||||
@Query("select u from User u where u.firstname like %?#{#escape([0],'#')}% escape '#'")
|
||||
List<User> findContainingEscaped(String namePart);
|
||||
|
||||
interface RolesAndFirstname {
|
||||
|
||||
String getFirstname();
|
||||
|
||||
Reference in New Issue
Block a user