DATAJPA-1534 - Escape wildcards in QBE Like-queries.

The `StringMatcher` values `STARTING`, `ENDING`, and `CONTAINING` now escape wildcards in their parameters.

See also: DATAJPA-1519, DATAJPA-1522.
This commit is contained in:
Jens Schauder
2019-04-26 07:35:50 +02:00
parent 0d0a93a4a2
commit d5f8816506
5 changed files with 128 additions and 26 deletions

View File

@@ -36,6 +36,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.repository.core.support.ExampleMatcherAccessor; import org.springframework.data.repository.core.support.ExampleMatcherAccessor;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@@ -55,6 +56,7 @@ import org.springframework.util.StringUtils;
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Oliver Gierke * @author Oliver Gierke
* @author Jens Schauder
* @since 1.10 * @since 1.10
*/ */
public class QueryByExamplePredicateBuilder { public class QueryByExamplePredicateBuilder {
@@ -76,9 +78,11 @@ public class QueryByExamplePredicateBuilder {
* @param root must not be {@literal null}. * @param root must not be {@literal null}.
* @param cb must not be {@literal null}. * @param cb must not be {@literal null}.
* @param example must not be {@literal null}. * @param example must not be {@literal null}.
* @param escapeCharacter
* @return never {@literal null}. * @return never {@literal null}.
*/ */
public static <T> Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Example<T> example) { public static <T> Predicate getPredicate(Root<T> root, CriteriaBuilder cb, Example<T> example,
EscapeCharacter escapeCharacter) {
Assert.notNull(root, "Root must not be null!"); Assert.notNull(root, "Root must not be null!");
Assert.notNull(cb, "CriteriaBuilder must not be null!"); Assert.notNull(cb, "CriteriaBuilder must not be null!");
@@ -87,7 +91,8 @@ public class QueryByExamplePredicateBuilder {
ExampleMatcher matcher = example.getMatcher(); ExampleMatcher matcher = example.getMatcher();
List<Predicate> predicates = getPredicates("", cb, root, root.getModel(), example.getProbe(), List<Predicate> 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()) { if (predicates.isEmpty()) {
return cb.isTrue(cb.literal(true)); return cb.isTrue(cb.literal(true));
@@ -104,7 +109,8 @@ public class QueryByExamplePredicateBuilder {
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
static List<Predicate> getPredicates(String path, CriteriaBuilder cb, Path<?> from, ManagedType<?> type, Object value, static List<Predicate> 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<Predicate> predicates = new ArrayList<>(); List<Predicate> predicates = new ArrayList<>();
DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(value); DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper(value);
@@ -133,8 +139,9 @@ public class QueryByExamplePredicateBuilder {
if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) { if (attribute.getPersistentAttributeType().equals(PersistentAttributeType.EMBEDDED)) {
predicates.addAll(getPredicates(currentPath, cb, from.get(attribute.getName()), predicates
(ManagedType<?>) attribute.getType(), attributeValue, probeType, exampleAccessor, currentNode)); .addAll(getPredicates(currentPath, cb, from.get(attribute.getName()), (ManagedType<?>) attribute.getType(),
attributeValue, probeType, exampleAccessor, currentNode, escapeCharacter));
continue; continue;
} }
@@ -153,7 +160,7 @@ public class QueryByExamplePredicateBuilder {
} }
predicates.addAll(getPredicates(currentPath, cb, ((From<?, ?>) from).join(attribute.getName()), 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; continue;
} }
@@ -173,13 +180,25 @@ public class QueryByExamplePredicateBuilder {
predicates.add(cb.equal(expression, attributeValue)); predicates.add(cb.equal(expression, attributeValue));
break; break;
case CONTAINING: case CONTAINING:
predicates.add(cb.like(expression, "%" + attributeValue + "%")); predicates.add(cb.like( //
expression, //
"%" + escapeCharacter.escape(attributeValue.toString()) + "%", //
escapeCharacter.getEscapeCharacter() //
));
break; break;
case STARTING: case STARTING:
predicates.add(cb.like(expression, attributeValue + "%")); predicates.add(cb.like(//
expression, //
escapeCharacter.escape(attributeValue.toString()) + "%", //
escapeCharacter.getEscapeCharacter()) //
);
break; break;
case ENDING: case ENDING:
predicates.add(cb.like(expression, "%" + attributeValue)); predicates.add(cb.like( //
expression, //
"%" + escapeCharacter.escape(attributeValue.toString()), //
escapeCharacter.getEscapeCharacter()) //
);
break; break;
default: default:
throw new IllegalArgumentException( throw new IllegalArgumentException(

View File

@@ -133,6 +133,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport {
JpaRepositoryImplementation<?, ?> repository = getTargetRepository(information, entityManager); JpaRepositoryImplementation<?, ?> repository = getTargetRepository(information, entityManager);
repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata());
repository.setEscapeCharacter(escapeCharacter);
return repository; return repository;
} }

View File

@@ -17,6 +17,7 @@ package org.springframework.data.jpa.repository.support;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.NoRepositoryBean;
/** /**
@@ -24,6 +25,7 @@ import org.springframework.data.repository.NoRepositoryBean;
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Stefan Fussenegger * @author Stefan Fussenegger
* @author Jens Schauder
*/ */
@NoRepositoryBean @NoRepositoryBean
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> { public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
@@ -34,4 +36,11 @@ public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>
* @param crudMethodMetadata must not be {@literal null}. * @param crudMethodMetadata must not be {@literal null}.
*/ */
void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata); 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);
} }

View File

@@ -48,6 +48,7 @@ import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.EntityGraph; 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.query.QueryUtils;
import org.springframework.data.jpa.repository.support.QueryHints.NoHints; import org.springframework.data.jpa.repository.support.QueryHints.NoHints;
import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.data.repository.support.PageableExecutionUtils;
@@ -81,6 +82,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
private final PersistenceProvider provider; private final PersistenceProvider provider;
private @Nullable CrudMethodMetadata metadata; private @Nullable CrudMethodMetadata metadata;
private EscapeCharacter escapeCharacter;
/** /**
* Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}. * Creates a new {@link SimpleJpaRepository} to manage objects of the given {@link JpaEntityInformation}.
@@ -118,6 +120,11 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
this.metadata = crudMethodMetadata; this.metadata = crudMethodMetadata;
} }
@Override
public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
this.escapeCharacter = escapeCharacter;
}
@Nullable @Nullable
protected CrudMethodMetadata getRepositoryMethodMetadata() { protected CrudMethodMetadata getRepositoryMethodMetadata() {
return metadata; return metadata;
@@ -405,7 +412,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
try { try {
return Optional.of( return Optional.of(
getQuery(new ExampleSpecification<S>(example), example.getProbeType(), Sort.unsorted()).getSingleResult()); getQuery(new ExampleSpecification<S>(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getSingleResult());
} catch (NoResultException e) { } catch (NoResultException e) {
return Optional.empty(); return Optional.empty();
} }
@@ -417,7 +424,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
*/ */
@Override @Override
public <S extends T> long count(Example<S> example) { public <S extends T> long count(Example<S> example) {
return executeCountQuery(getCountQuery(new ExampleSpecification<S>(example), example.getProbeType())); return executeCountQuery(getCountQuery(new ExampleSpecification<S>(example, escapeCharacter), example.getProbeType()));
} }
/* /*
@@ -426,7 +433,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
*/ */
@Override @Override
public <S extends T> boolean exists(Example<S> example) { public <S extends T> boolean exists(Example<S> example) {
return !getQuery(new ExampleSpecification<S>(example), example.getProbeType(), Sort.unsorted()).getResultList() return !getQuery(new ExampleSpecification<S>(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getResultList()
.isEmpty(); .isEmpty();
} }
@@ -436,7 +443,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
*/ */
@Override @Override
public <S extends T> List<S> findAll(Example<S> example) { public <S extends T> List<S> findAll(Example<S> example) {
return getQuery(new ExampleSpecification<S>(example), example.getProbeType(), Sort.unsorted()).getResultList(); return getQuery(new ExampleSpecification<S>(example, escapeCharacter), example.getProbeType(), Sort.unsorted()).getResultList();
} }
/* /*
@@ -445,7 +452,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
*/ */
@Override @Override
public <S extends T> List<S> findAll(Example<S> example, Sort sort) { public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
return getQuery(new ExampleSpecification<S>(example), example.getProbeType(), sort).getResultList(); return getQuery(new ExampleSpecification<S>(example, escapeCharacter), example.getProbeType(), sort).getResultList();
} }
/* /*
@@ -455,9 +462,9 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
@Override @Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) { public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
ExampleSpecification<S> spec = new ExampleSpecification<>(example); ExampleSpecification<S> spec = new ExampleSpecification<>(example, escapeCharacter);
Class<S> probeType = example.getProbeType(); Class<S> probeType = example.getProbeType();
TypedQuery<S> query = getQuery(new ExampleSpecification<>(example), probeType, pageable); TypedQuery<S> query = getQuery(new ExampleSpecification<>(example, escapeCharacter), probeType, pageable);
return isUnpaged(pageable) ? new PageImpl<>(query.getResultList()) : readPage(query, probeType, pageable, spec); return isUnpaged(pageable) ? new PageImpl<>(query.getResultList()) : readPage(query, probeType, pageable, spec);
} }
@@ -791,16 +798,21 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final Example<T> example; private final Example<T> example;
private final EscapeCharacter escapeCharacter;
/** /**
* Creates new {@link ExampleSpecification}. * Creates new {@link ExampleSpecification}.
* *
* @param example * @param example
* @param escapeCharacter
*/ */
ExampleSpecification(Example<T> example) { ExampleSpecification(Example<T> example, EscapeCharacter escapeCharacter) {
Assert.notNull(example, "Example must not be null!"); Assert.notNull(example, "Example must not be null!");
Assert.notNull(escapeCharacter, "EscapeCharacter must not be null!");
this.example = example; this.example = example;
this.escapeCharacter = escapeCharacter;
} }
/* /*
@@ -809,7 +821,7 @@ public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T
*/ */
@Override @Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) { public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return QueryByExamplePredicateBuilder.getPredicate(root, cb, example); return QueryByExamplePredicateBuilder.getPredicate(root, cb, example, escapeCharacter);
} }
} }
} }

View File

@@ -48,6 +48,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@@ -56,6 +57,7 @@ import org.springframework.util.ObjectUtils;
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch * @author Mark Paluch
* @author Oliver Gierke * @author Oliver Gierke
* @author Jens Schauder
*/ */
@RunWith(MockitoJUnitRunner.Silent.class) @RunWith(MockitoJUnitRunner.Silent.class)
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
@@ -118,22 +120,23 @@ public class QueryByExamplePredicateBuilderUnitTests {
@Test(expected = IllegalArgumentException.class) // DATAJPA-218 @Test(expected = IllegalArgumentException.class) // DATAJPA-218
public void getPredicateShouldThrowExceptionOnNullRoot() { 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 @Test(expected = IllegalArgumentException.class) // DATAJPA-218
public void getPredicateShouldThrowExceptionOnNullCriteriaBuilder() { 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 @Test(expected = IllegalArgumentException.class) // DATAJPA-218
public void getPredicateShouldThrowExceptionOnNullExample() { public void getPredicateShouldThrowExceptionOnNullExample() {
QueryByExamplePredicateBuilder.getPredicate(root, null, null); QueryByExamplePredicateBuilder.getPredicate(root, null, null, EscapeCharacter.of('\\'));
} }
@Test // DATAJPA-218 @Test // DATAJPA-218
public void emptyCriteriaListShouldResultTruePredicate() { 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 @Test // DATAJPA-218
@@ -142,7 +145,8 @@ public class QueryByExamplePredicateBuilderUnitTests {
Person p = new Person(); Person p = new Person();
p.firstname = "foo"; 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")); verify(cb, times(1)).equal(any(Expression.class), eq("foo"));
} }
@@ -157,7 +161,7 @@ public class QueryByExamplePredicateBuilderUnitTests {
exception.expectCause(IsInstanceOf.<Throwable> instanceOf(IllegalArgumentException.class)); exception.expectCause(IsInstanceOf.<Throwable> instanceOf(IllegalArgumentException.class));
exception.expectMessage("Unexpected path type"); exception.expectMessage("Unexpected path type");
QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p)); QueryByExamplePredicateBuilder.getPredicate(root, cb, of(p), EscapeCharacter.of('\\'));
} }
@Test // DATAJPA-218 @Test // DATAJPA-218
@@ -167,7 +171,8 @@ public class QueryByExamplePredicateBuilderUnitTests {
p.firstname = "foo"; p.firstname = "foo";
p.age = 2L; 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("foo"));
verify(cb, times(1)).equal(any(Expression.class), eq(2L)); verify(cb, times(1)).equal(any(Expression.class), eq(2L));
@@ -182,11 +187,67 @@ public class QueryByExamplePredicateBuilderUnitTests {
Example<Person> example = of(person, ExampleMatcher.matchingAny()); Example<Person> 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()); verify(cb, times(1)).or(ArgumentMatchers.any());
} }
@Test // DATAJPA-1534
public void likePatternsGetEscapedContaining() {
Person person = new Person();
person.firstname = "f\\o_o";
Example<Person> 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<Person> 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<Person> 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 { static class Person {
@Id Long id; @Id Long id;