diff --git a/pom.xml b/pom.xml index 80f474f5a..1255f0c91 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 c52d64d62..54d239c73 100644 --- a/src/main/asciidoc/jpa.adoc +++ b/src/main/asciidoc/jpa.adoc @@ -123,6 +123,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). @@ -427,6 +432,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 3ae205558..06c82afc4 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 @@ -150,4 +150,12 @@ public @interface EnableJpaRepositories { * @return whether to enable default transactions, defaults to {@literal true}. */ boolean enableDefaultTransactions() default true; + + /** + * 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 7019981e4..90cced9ab 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 dd2754add..cbfa21869 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, - EvaluationContextProvider evaluationContextProvider) { + EvaluationContextProvider 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 e6ed40825..51e26cc83 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,6 +55,7 @@ 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 @@ -63,22 +65,24 @@ class ParameterMetadataProvider { * @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 +94,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 +108,7 @@ class ParameterMetadataProvider { this.expressions = new ArrayList<>(); this.bindableParameterValues = bindableParameterValues; this.persistenceProvider = provider; + this.escape = escape; } /** @@ -170,12 +176,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 +198,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 +244,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 a9dd96bed..9f628ee39 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,6 +54,7 @@ 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}. @@ -60,12 +62,14 @@ public class PartTreeJpaQuery extends AbstractJpaQuery { * @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 +230,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 +284,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 c45d0eb0a..f1923664f 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; @@ -28,11 +29,13 @@ import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindin import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.jpa.repository.support.EscapeCharacter; import org.springframework.expression.EvaluationContext; 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 @@ -174,6 +177,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 5d1c59db3..5534d446d 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 @@ -48,7 +48,7 @@ import org.springframework.util.Assert; /** * JPA specific generic repository factory. - * + * * @author Oliver Gierke * @author Mark Paluch * @author Christoph Strobl @@ -60,9 +60,11 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private final QueryExtractor extractor; private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; + private EscapeCharacter escapeCharacter = EscapeCharacter.of('\\'); + /** * Creates a new {@link JpaRepositoryFactory}. - * + * * @param entityManager must not be {@literal null} */ public JpaRepositoryFactory(EntityManager entityManager) { @@ -80,7 +82,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { } } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#setBeanClassLoader(java.lang.ClassLoader) */ @@ -91,6 +93,15 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } + /** + * 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) @@ -106,7 +117,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { /** * Callback to create a {@link JpaRepository} instance with the given {@link EntityManager} - * + * * @param * @param * @param entityManager @@ -150,7 +161,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { /** * Returns whether the given repository interface requires a QueryDsl specific implementation to be chosen. - * + * * @param repositoryInterface * @return */ @@ -159,14 +170,15 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(repositoryInterface); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) */ @Override protected Optional getQueryLookupStrategy(@Nullable Key key, EvaluationContextProvider evaluationContextProvider) { - return Optional.of(JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider)); + return Optional + .of(JpaQueryLookupStrategy.create(entityManager, key, extractor, evaluationContextProvider, escapeCharacter)); } /* @@ -201,7 +213,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { /** * Creates a new {@link EclipseLinkProjectionQueryCreationListener} for the given {@link EntityManager}. - * + * * @param em must not be {@literal null}. */ public EclipseLinkProjectionQueryCreationListener(EntityManager em) { @@ -211,7 +223,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.metamodel = JpaMetamodel.of(em.getMetamodel()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.QueryCreationListener#onCreation(org.springframework.data.repository.query.RepositoryQuery) */ 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 f30efca23..2595d7999 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 @@ -38,7 +38,6 @@ public class JpaRepositoryFactoryBean, S, ID> extends TransactionalRepositoryFactoryBeanSupport { private @Nullable EntityManager entityManager; - /** * Creates a new {@link JpaRepositoryFactoryBean} for the given repository interface. * @@ -88,7 +87,11 @@ public class JpaRepositoryFactoryBean, S, ID> * @return */ protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { - return new JpaRepositoryFactory(entityManager); + + JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); + jpaRepositoryFactory.setEscapeCharacter(escapeCharacter); + + return jpaRepositoryFactory; } /* @@ -101,4 +104,9 @@ public class JpaRepositoryFactoryBean, S, ID> Assert.state(entityManager != null,"EntityManager must not be null!"); 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 8865aa9fc..1991c582c 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 916f215dd..4e48330f2 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 a42bd858d..f038b8296 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 307f6c679..12d86f00e 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 85ced0495..d3ffe80db 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 58c1f2f1e..4d79261d7 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 24a3bb841..d74575259 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 7633ae3a8..cce006b3d 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 @@ -556,6 +556,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();