diff --git a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java index 170ba477f..6b8428e41 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -36,6 +36,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.lang.Nullable; @@ -55,6 +56,7 @@ import org.springframework.util.StringUtils; * @author Christoph Strobl * @author Mark Paluch * @author Oliver Gierke + * @author Jens Schauder * @since 1.10 */ public class QueryByExamplePredicateBuilder { @@ -74,9 +76,11 @@ public class QueryByExamplePredicateBuilder { * @param root must not be {@literal null}. * @param cb must not be {@literal null}. * @param example must not be {@literal null}. + * @param escapeCharacter * @return never {@literal null}. */ - public static Predicate getPredicate(Root root, CriteriaBuilder cb, Example example) { + public static Predicate getPredicate(Root root, CriteriaBuilder cb, Example example, + EscapeCharacter escapeCharacter) { Assert.notNull(root, "Root must not be null!"); Assert.notNull(cb, "CriteriaBuilder must not be null!"); @@ -85,7 +89,8 @@ public class QueryByExamplePredicateBuilder { ExampleMatcher matcher = example.getMatcher(); List predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(), - example.getProbeType(), new ExampleMatcherAccessor(matcher), new PathNode("root", null, example.getProbe())); + example.getProbeType(), new ExampleMatcherAccessor(matcher), new PathNode("root", null, example.getProbe()), + escapeCharacter); if (predicates.isEmpty()) { return cb.isTrue(cb.literal(true)); @@ -102,7 +107,8 @@ public class QueryByExamplePredicateBuilder { @SuppressWarnings({ "rawtypes", "unchecked" }) static List getPredicates(String path, CriteriaBuilder cb, Path from, ManagedType type, Object value, - Class probeType, ExampleMatcherAccessor exampleAccessor, PathNode currentNode) { + Class probeType, ExampleMatcherAccessor exampleAccessor, PathNode currentNode, + EscapeCharacter escapeCharacter) { List predicates = new ArrayList<>(); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(value); @@ -131,8 +137,9 @@ public class QueryByExamplePredicateBuilder { if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) { - predicates.addAll(getPredicates(currentPath, cb, from.get(attribute.getName()), - (ManagedType) attribute.getType(), attributeValue, probeType, exampleAccessor, currentNode)); + predicates + .addAll(getPredicates(currentPath, cb, from.get(attribute.getName()), (ManagedType) attribute.getType(), + attributeValue, probeType, exampleAccessor, currentNode, escapeCharacter)); continue; } @@ -151,7 +158,7 @@ public class QueryByExamplePredicateBuilder { } predicates.addAll(getPredicates(currentPath, cb, ((From) from).join(attribute.getName()), - (ManagedType) attribute.getType(), attributeValue, probeType, exampleAccessor, node)); + (ManagedType) attribute.getType(), attributeValue, probeType, exampleAccessor, node, escapeCharacter)); continue; } @@ -171,13 +178,25 @@ public class QueryByExamplePredicateBuilder { predicates.add(cb.equal(expression, attributeValue)); break; case CONTAINING: - predicates.add(cb.like(expression, "%" + attributeValue + "%")); + predicates.add(cb.like( // + expression, // + "%" + escapeCharacter.escape(attributeValue.toString()) + "%", // + escapeCharacter.getEscapeCharacter() // + )); break; case STARTING: - predicates.add(cb.like(expression, attributeValue + "%")); + predicates.add(cb.like(// + expression, // + escapeCharacter.escape(attributeValue.toString()) + "%", // + escapeCharacter.getEscapeCharacter()) // + ); break; case ENDING: - predicates.add(cb.like(expression, "%" + attributeValue)); + predicates.add(cb.like( // + expression, // + "%" + escapeCharacter.escape(attributeValue.toString()), // + escapeCharacter.getEscapeCharacter()) // + ); break; default: throw new IllegalArgumentException( 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 1fde89b1a..5354a3601 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 @@ -133,6 +133,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { JpaRepositoryImplementation repository = getTargetRepository(information, entityManager); repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); + repository.setEscapeCharacter(escapeCharacter); return repository; } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java index 090c70ff2..918938bc2 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java @@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.support; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.repository.NoRepositoryBean; /** @@ -24,6 +25,7 @@ import org.springframework.data.repository.NoRepositoryBean; * * @author Oliver Gierke * @author Stefan Fussenegger + * @author Jens Schauder */ @NoRepositoryBean public interface JpaRepositoryImplementation extends JpaRepository, JpaSpecificationExecutor { @@ -34,4 +36,11 @@ public interface JpaRepositoryImplementation extends JpaRepository * @param crudMethodMetadata must not be {@literal null}. */ void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata); + + /** + * Configures the {@link EscapeCharacter} to be used with the repository. + * + * @param escapeCharacter Must not be {@literal null}. + */ + void setEscapeCharacter(EscapeCharacter escapeCharacter); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index a17f6320c..7d115980a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -48,6 +48,7 @@ import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.jpa.repository.support.QueryHints.NoHints; import org.springframework.data.repository.support.PageableExecutionUtils; @@ -81,6 +82,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation implements JpaRepositoryImplementation implements JpaRepositoryImplementation(example), example.getProbeType(), Sort.unsorted()).getSingleResult()); + getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getSingleResult()); } catch (NoResultException e) { return Optional.empty(); } @@ -434,7 +441,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation long count(Example example) { - return executeCountQuery(getCountQuery(new ExampleSpecification(example), example.getProbeType())); + return executeCountQuery(getCountQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType())); } /* @@ -443,7 +450,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation boolean exists(Example example) { - return !getQuery(new ExampleSpecification(example), example.getProbeType(), Sort.unsorted()).getResultList() + return !getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getResultList() .isEmpty(); } @@ -453,7 +460,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation List findAll(Example example) { - return getQuery(new ExampleSpecification(example), example.getProbeType(), Sort.unsorted()).getResultList(); + return getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getResultList(); } /* @@ -462,7 +469,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation List findAll(Example example, Sort sort) { - return getQuery(new ExampleSpecification(example), example.getProbeType(), sort).getResultList(); + return getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), sort).getResultList(); } /* @@ -472,9 +479,9 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation Page findAll(Example example, Pageable pageable) { - ExampleSpecification spec = new ExampleSpecification<>(example); + ExampleSpecification spec = new ExampleSpecification<>(example, escapeCharacter); Class probeType = example.getProbeType(); - TypedQuery query = getQuery(new ExampleSpecification<>(example), probeType, pageable); + TypedQuery query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable); return isUnpaged(pageable) ? new PageImpl<>(query.getResultList()) : readPage(query, probeType, pageable, spec); } @@ -815,16 +822,21 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation example; + private final EscapeCharacter escapeCharacter; /** * Creates new {@link ExampleSpecification}. * * @param example + * @param escapeCharacter */ - ExampleSpecification(Example example) { + ExampleSpecification(Example example, EscapeCharacter escapeCharacter) { Assert.notNull(example, "Example must not be null!"); + Assert.notNull(escapeCharacter, "EscapeCharacter must not be null!"); + this.example = example; + this.escapeCharacter = escapeCharacter; } /* @@ -833,7 +845,7 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation root, CriteriaQuery query, CriteriaBuilder cb) { - return QueryByExamplePredicateBuilder.getPredicate(root, cb, example); + return QueryByExamplePredicateBuilder.getPredicate(root, cb, example, escapeCharacter); } } } diff --git a/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java index 4fe39685c..e46f6e66e 100644 --- a/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilderUnitTests.java @@ -48,6 +48,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.util.ObjectUtils; /** @@ -56,6 +57,7 @@ import org.springframework.util.ObjectUtils; * @author Christoph Strobl * @author Mark Paluch * @author Oliver Gierke + * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.Silent.class) @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -118,22 +120,23 @@ public class QueryByExamplePredicateBuilderUnitTests { @Test(expected = IllegalArgumentException.class) // DATAJPA-218 public void getPredicateShouldThrowExceptionOnNullRoot() { - QueryByExamplePredicateBuilder.getPredicate(null, cb, of(new Person())); + QueryByExamplePredicateBuilder.getPredicate(null, cb, of(new Person()), EscapeCharacter.of('\\')); } @Test(expected = IllegalArgumentException.class) // DATAJPA-218 public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() { - QueryByExamplePredicateBuilder.getPredicate(root, null, of(new Person())); + QueryByExamplePredicateBuilder.getPredicate(root, null, of(new Person()), EscapeCharacter.of('\\')); } @Test(expected = IllegalArgumentException.class) // DATAJPA-218 public void getPredicateShouldThrowExceptionOnNullExample() { - QueryByExamplePredicateBuilder.getPredicate(root, null, null); + QueryByExamplePredicateBuilder.getPredicate(root, null, null, EscapeCharacter.of('\\')); } @Test // DATAJPA-218 public void emptyCriteriaListShouldResultTruePredicate() { - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person())), equalTo(truePredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(new Person()), EscapeCharacter.of('\\')), + equalTo(truePredicate)); } @Test // DATAJPA-218 @@ -142,7 +145,8 @@ public class QueryByExamplePredicateBuilderUnitTests { Person p = new Person(); p.firstname = "foo"; - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)), equalTo(dummyPredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p), EscapeCharacter.of('\\')), + equalTo(dummyPredicate)); verify(cb, times(1)).equal(any(Expression.class), eq("foo")); } @@ -157,7 +161,7 @@ public class QueryByExamplePredicateBuilderUnitTests { exception.expectCause(IsInstanceOf. instanceOf(IllegalArgumentException.class)); exception.expectMessage("Unexpected path type"); - QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)); + QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p), EscapeCharacter.of('\\')); } @Test // DATAJPA-218 @@ -167,7 +171,8 @@ public class QueryByExamplePredicateBuilderUnitTests { p.firstname = "foo"; p.age = 2L; - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)), equalTo(andPredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p), EscapeCharacter.of('\\')), + equalTo(andPredicate)); verify(cb, times(1)).equal(any(Expression.class), eq("foo")); verify(cb, times(1)).equal(any(Expression.class), eq(2L)); @@ -182,11 +187,67 @@ public class QueryByExamplePredicateBuilderUnitTests { Example example = of(person, ExampleMatcher.matchingAny()); - assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, example), equalTo(orPredicate)); + assertThat(QueryByExamplePredicateBuilder.getPredicate(root, cb, example, EscapeCharacter.of('\\')), + equalTo(orPredicate)); verify(cb, times(1)).or(ArgumentMatchers.any()); } + @Test // DATAJPA-1534 + public void likePatternsGetEscapedContaining() { + + Person person = new Person(); + person.firstname = "f\\o_o"; + + Example example = of( // + person, // + ExampleMatcher // + .matchingAny() // + .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // + ); + + QueryByExamplePredicateBuilder.getPredicate(root, cb, example, EscapeCharacter.of('\\')); + + verify(cb, times(1)).like(any(Expression.class), eq("%f\\\\o\\_o%"), eq('\\')); + } + + @Test // DATAJPA-1534 + + public void likePatternsGetEscapedStarting() { + + Person person = new Person(); + person.firstname = "f\\o_o"; + + Example example = of( // + person, // + ExampleMatcher // + .matchingAny() // + .withStringMatcher(ExampleMatcher.StringMatcher.STARTING) // + ); + + QueryByExamplePredicateBuilder.getPredicate(root, cb, example, EscapeCharacter.of('\\')); + + verify(cb, times(1)).like(any(Expression.class), eq("f\\\\o\\_o%"), eq('\\')); + } + + @Test // DATAJPA-1534 + public void likePatternsGetEscapedEnding() { + + Person person = new Person(); + person.firstname = "f\\o_o"; + + Example example = of( // + person, // + ExampleMatcher // + .matchingAny() // + .withStringMatcher(ExampleMatcher.StringMatcher.ENDING) // + ); + + QueryByExamplePredicateBuilder.getPredicate(root, cb, example, EscapeCharacter.of('\\')); + + verify(cb, times(1)).like(any(Expression.class), eq("%f\\\\o\\_o"), eq('\\')); + } + static class Person { @Id Long id;