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 9386d2928..58ff56d73 100644 --- a/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java +++ b/src/main/java/org/springframework/data/jpa/convert/QueryByExamplePredicateBuilder.java @@ -18,7 +18,6 @@ package org.springframework.data.jpa.convert; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -import java.util.EnumSet; import java.util.List; import java.util.Set; @@ -36,6 +35,7 @@ import javax.persistence.metamodel.SingularAttribute; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.repository.core.support.ExampleMatcherAccessor; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.orm.jpa.JpaSystemException; @@ -54,6 +54,7 @@ import org.springframework.util.StringUtils; * @author Christoph Strobl * @author Mark Paluch * @author Oliver Gierke + * @author Jens Schauder * @since 1.10 */ public class QueryByExamplePredicateBuilder { @@ -71,9 +72,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!"); @@ -82,7 +85,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)); @@ -99,7 +103,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); @@ -125,16 +130,17 @@ 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; } if (isAssociation(attribute)) { if (!(from instanceof From)) { - throw new JpaSystemException(new IllegalArgumentException( - String.format("Unexpected path type for %s. Found %s where From.class was expected.", currentPath, from))); + throw new JpaSystemException(new IllegalArgumentException(String + .format("Unexpected path type for %s. Found %s where From.class was expected.", currentPath, from))); } PathNode node = currentNode.add(attribute.getName(), attributeValue); @@ -145,7 +151,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; } @@ -165,13 +171,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 30a39a80e..708463079 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 @@ -110,6 +110,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { SimpleJpaRepository 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/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index cde8dbcc5..7454bed15 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 @@ -51,6 +51,7 @@ import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; 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.jpa.repository.query.Jpa21Utils; import org.springframework.data.jpa.repository.query.JpaEntityGraph; import org.springframework.data.jpa.repository.query.QueryUtils; @@ -63,7 +64,7 @@ import org.springframework.util.Assert; /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer * you a more sophisticated interface than the plain {@link EntityManager} . - * + * * @author Oliver Gierke * @author Eberhard Wolff * @author Thomas Darimont @@ -83,10 +84,11 @@ public class SimpleJpaRepository private final PersistenceProvider provider; private CrudMethodMetadata metadata; + private EscapeCharacter escapeCharacter; /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}. - * + * * @param entityInformation must not be {@literal null}. * @param entityManager must not be {@literal null}. */ @@ -102,7 +104,7 @@ public class SimpleJpaRepository /** * Creates a new {@link SimpleJpaRepository} to manage objects of the given domain type. - * + * * @param domainClass must not be {@literal null}. * @param em must not be {@literal null}. */ @@ -113,13 +115,17 @@ public class SimpleJpaRepository /** * Configures a custom {@link CrudMethodMetadata} to be used to detect {@link LockModeType}s and query hints to be * applied to queries. - * + * * @param crudMethodMetadata */ public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { this.metadata = crudMethodMetadata; } + public void setEscapeCharacter(EscapeCharacter escapeCharacter) { + this.escapeCharacter = escapeCharacter; + } + protected CrudMethodMetadata getRepositoryMethodMetadata() { return metadata; } @@ -211,7 +217,7 @@ public class SimpleJpaRepository } } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#deleteAllInBatch() */ @@ -244,7 +250,7 @@ public class SimpleJpaRepository /** * Returns a {@link Map} with the query hints based on the current {@link CrudMethodMetadata} and potential * {@link EntityGraph} information. - * + * * @return */ protected Map getQueryHints() { @@ -267,7 +273,7 @@ public class SimpleJpaRepository return new JpaEntityGraph(metadata.getEntityGraph(), fallbackName); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaRepository#getOne(java.io.Serializable) */ @@ -417,35 +423,35 @@ public class SimpleJpaRepository return getQuery(spec, sort).getResultList(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example) */ @Override public S findOne(Example example) { try { - return getQuery(new ExampleSpecification(example), example.getProbeType(), (Sort) null).getSingleResult(); + return getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), (Sort) null).getSingleResult(); } catch (NoResultException e) { return null; } } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#count(org.springframework.data.domain.Example) */ @Override public long count(Example example) { - return executeCountQuery(getCountQuery(new ExampleSpecification(example), example.getProbeType())); + return executeCountQuery(getCountQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#exists(org.springframework.data.domain.Example) */ @Override public boolean exists(Example example) { - return !getQuery(new ExampleSpecification(example), example.getProbeType(), (Sort) null).getResultList() + return !getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), (Sort) null).getResultList() .isEmpty(); } @@ -455,7 +461,7 @@ public class SimpleJpaRepository */ @Override public List findAll(Example example) { - return getQuery(new ExampleSpecification(example), example.getProbeType(), (Sort) null).getResultList(); + return getQuery(new ExampleSpecification(example, escapeCharacter), example.getProbeType(), (Sort) null).getResultList(); } /* @@ -464,7 +470,7 @@ public class SimpleJpaRepository */ @Override public 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(); } /* @@ -474,9 +480,9 @@ public class SimpleJpaRepository @Override public 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 pageable == null ? new PageImpl(query.getResultList()) : readPage(query, probeType, pageable, spec); } @@ -624,7 +630,7 @@ public class SimpleJpaRepository /** * Creates a {@link TypedQuery} for the given {@link Specification} and {@link Sort}. - * + * * @param spec can be {@literal null}. * @param sort can be {@literal null}. * @return @@ -658,7 +664,7 @@ public class SimpleJpaRepository /** * Creates a new count query for the given {@link Specification}. - * + * * @param spec can be {@literal null}. * @return * @deprecated override {@link #getCountQuery(Specification, Class)} instead @@ -747,7 +753,7 @@ public class SimpleJpaRepository /** * Executes a count query and transparently sums up all values returned. - * + * * @param query must not be {@literal null}. * @return */ @@ -769,7 +775,7 @@ public class SimpleJpaRepository * Specification that gives access to the {@link Parameter} instance used to bind the ids for * {@link SimpleJpaRepository#findAll(Iterable)}. Workaround for OpenJPA not binding collections to in-clauses * correctly when using by-name binding. - * + * * @see OPENJPA-2018 * @author Oliver Gierke */ @@ -807,16 +813,21 @@ public class SimpleJpaRepository private static class ExampleSpecification implements Specification { private final Example example; + private final EscapeCharacter escapeCharacter; /** * Creates new {@link ExampleSpecification}. * * @param example + * @param escapeCharacter */ - public ExampleSpecification(Example example) { + public 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; } /* @@ -825,7 +836,7 @@ public class SimpleJpaRepository */ @Override public Predicate toPredicate(Root 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 50dbb2016..5277ddcac 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.runners.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.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(Matchers.anyVararg()); } + @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;