diff --git a/pom.xml b/pom.xml index a19cb3a82..310678df8 100644 --- a/pom.xml +++ b/pom.xml @@ -211,7 +211,7 @@ org.hsqldb hsqldb - 2.2.8 + 2.4.1 test 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 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 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 metadata = new ParameterMetadata<>(expression, part.getType(), value, persistenceProvider); + ParameterMetadata 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 expression; private final PersistenceProvider persistenceProvider; + private final EscapeCharacter escape; /** * Creates a new {@link ParameterMetadata}. */ public ParameterMetadata(ParameterExpression 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; } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java index b9e5af6a4..38ead40a3 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java @@ -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); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java index cc6b04b5e..d45ddc1b4 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java @@ -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); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java b/src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java new file mode 100644 index 000000000..21d9b107d --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/support/EscapeCharacter.java @@ -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); + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index 77afd9800..5e98c33c8 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -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 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)); } /* diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 2e14927b3..ad8dc4c66 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -44,6 +44,7 @@ public class JpaRepositoryFactoryBean, 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, S, ID> JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); jpaRepositoryFactory.setEntityPathResolver(entityPathResolver); - + jpaRepositoryFactory.setEscapeCharacter(escapeCharacter); return jpaRepositoryFactory; } @@ -118,4 +119,9 @@ public class JpaRepositoryFactoryBean, S, ID> super.afterPropertiesSet(); } + + public void setEscapeCharacter(char escapeCharacter) { + + this.escapeCharacter = EscapeCharacter.of(escapeCharacter); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java index 50b7c810c..52f886e46 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryFinderTests.java @@ -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() { diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java index 09854aede..5a4f8c0ab 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaCountQueryCreatorIntegrationTests.java @@ -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); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index d766683a4..d31554efa 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -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); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java index 6c85cef8c..96bcf08c0 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterExpressionProviderTests.java @@ -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 expression = provider.next(part, Comparable.class).getExpression(); assertThat(expression.getParameterType(), is(typeCompatibleWith(int.class))); } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java index 95376c536..9ddcfc711 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderIntegrationTests.java @@ -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" }) diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java index e49e359a8..ea7a9f74f 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterMetadataProviderUnitTests.java @@ -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))) // diff --git a/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java index cb0526e63..6a2296a85 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/PartTreeJpaQueryIntegrationTests.java @@ -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); } diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index b116952dd..191b26212 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -558,6 +558,10 @@ public interface UserRepository // DATAJPA-1334 List findByNamedQueryWithConstructorExpression(); + // DATAJPA-1519 + @Query("select u from User u where u.firstname like %?#{#escape([0],'#')}% escape '#'") + List findContainingEscaped(String namePart); + interface RolesAndFirstname { String getFirstname();