From d41a9cb5c5af1bb7bdcb34d9d55b7b1ebc4ccede Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 30 Jun 2016 08:54:47 +0200 Subject: [PATCH] DATACMNS-879 - ExampleMatcher now allows to define whether all or any match should be used. Introduced dedicated factory methods for ExampleMatcher so that it exposes whether the predicates built from Example instances have to be fulfilled all or if it's sufficient that any of them matches. --- src/main/asciidoc/query-by-example.adoc | 6 +- .../data/domain/ExampleMatcher.java | 95 +++++++++++++++---- .../data/domain/ExampleMatcherUnitTests.java | 31 ++++++ 3 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc index ab63fd71f..384ec44ca 100644 --- a/src/main/asciidoc/query-by-example.adoc +++ b/src/main/asciidoc/query-by-example.adoc @@ -27,7 +27,6 @@ Query by Example is suited for several use-cases but also comes with limitations **Limitations** -* Query predicates are combined using the `AND` keyword * No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` * Only supports starts/contains/ends/regex matching for strings and exact matching for other property types @@ -107,13 +106,15 @@ Example example = Example.of(person, matcher); <7> ---- <1> Create a new instance of the domain object. <2> Set properties. -<3> Create an `ExampleMatcher` which is usable at this stage even without further configuration. +<3> Create an `ExampleMatcher` to expect all values to match. It's usable at this stage even without further configuration. <4> Construct a new `ExampleMatcher` to ignore the property path `lastname`. <5> Construct a new `ExampleMatcher` to ignore the property path `lastname` and to include null values. <6> Construct a new `ExampleMatcher` to ignore the property path `lastname`, to include null values, and use perform suffix string matching. <7> Create a new `Example` based on the domain object and the configured `ExampleMatcher`. ==== +By default the `ExampleMatcher` will expect all values set on the probe to match. If you want to get results matching any of the predicates defined implicitly, use `ExampleMatcher.matchingAny()`. + You can specify behavior for individual properties (e.g. "firstname" and "lastname", "address.city" for nested properties). You can tune it with matching options and case sensitivity. .Configuring matcher options @@ -164,4 +165,3 @@ Queries created by `Example` use a merged view of the configuration. Default mat | Property path |=== - diff --git a/src/main/java/org/springframework/data/domain/ExampleMatcher.java b/src/main/java/org/springframework/data/domain/ExampleMatcher.java index ecced45ee..18a62bcc1 100644 --- a/src/main/java/org/springframework/data/domain/ExampleMatcher.java +++ b/src/main/java/org/springframework/data/domain/ExampleMatcher.java @@ -20,6 +20,7 @@ import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.FieldDefaults; +import lombok.experimental.Wither; import java.util.Arrays; import java.util.Collection; @@ -43,6 +44,7 @@ import org.springframework.util.Assert; * * @author Christoph Strobl * @author Mark Paluch + * @author Oliver Gierke * @param * @since 1.12 */ @@ -57,19 +59,45 @@ public class ExampleMatcher { PropertySpecifiers propertySpecifiers; Set ignoredPaths; boolean defaultIgnoreCase; + @Wither(AccessLevel.PRIVATE) MatchMode mode; private ExampleMatcher() { - this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections. emptySet(), false); + this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false, + MatchMode.ALL); } /** - * Create a new untyped {@link ExampleMatcher} including all non-null properties by default. + * Create a new {@link ExampleMatcher} including all non-null properties by default exposing that all resulting + * predicates are supposed to be AND-concatenated. * - * @param type must not be {@literal null}. + * @param type will never be {@literal null}. * @return + * @see #matchingAll() */ public static ExampleMatcher matching() { - return new ExampleMatcher(); + return matchingAll(); + } + + /** + * Create a new {@link ExampleMatcher} including all non-null properties by default matching any predicate derived + * from the example. + * + * @param type will never be {@literal null}. + * @return + */ + public static ExampleMatcher matchingAny() { + return new ExampleMatcher().withMode(MatchMode.ANY); + } + + /** + * Create a new {@link ExampleMatcher} including all non-null properties by default matching all predicates derived + * from the example. + * + * @param type will never be {@literal null}. + * @return + */ + public static ExampleMatcher matchingAll() { + return new ExampleMatcher().withMode(MatchMode.ALL); } /** @@ -87,8 +115,8 @@ public class ExampleMatcher { Set newIgnoredPaths = new LinkedHashSet(this.ignoredPaths); newIgnoredPaths.addAll(Arrays.asList(ignoredPaths)); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, - defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, defaultIgnoreCase, + mode); } /** @@ -102,7 +130,8 @@ public class ExampleMatcher { Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -123,7 +152,8 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase) { - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -175,7 +205,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -196,7 +227,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier.withValueTransformer(propertyValueTransformer)); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -218,7 +250,8 @@ public class ExampleMatcher { propertySpecifiers.add(propertySpecifier.withIgnoreCase(true)); } - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } private PropertySpecifier getOrCreatePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { @@ -237,8 +270,7 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIncludeNullValues() { - return new ExampleMatcher(NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return withNullHandler(NullHandler.INCLUDE); } /** @@ -248,8 +280,7 @@ public class ExampleMatcher { * @return */ public ExampleMatcher withIgnoreNullValues() { - return new ExampleMatcher(NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return withNullHandler(NullHandler.IGNORE); } /** @@ -262,7 +293,8 @@ public class ExampleMatcher { public ExampleMatcher withNullHandler(NullHandler nullHandler) { Assert.notNull(nullHandler, "NullHandler must not be null!"); - return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase, + mode); } /** @@ -312,6 +344,26 @@ public class ExampleMatcher { return propertySpecifiers; } + /** + * Returns whether all of the predicates of the {@link Example} are supposed to match. If {@literal false} is + * returned, it's sufficient if any of the predicates derived from the {@link Example} match. + * + * @return whether all of the predicates of the {@link Example} are supposed to match or any of them is sufficient. + */ + public boolean isAllMatching() { + return mode.equals(MatchMode.ALL); + } + + /** + * Returns whether it's sufficient that any of the predicates of the {@link Example} match. If {@literal false} is + * returned, all predicates derived from the example need to match to produce results. + * + * @return whether it's sufficient that any of the predicates of the {@link Example} match or all need to match. + */ + public boolean isAnyMatching() { + return mode.equals(MatchMode.ANY); + } + /** * Null handling for creating criterion out of an {@link Example}. * @@ -780,4 +832,15 @@ public class ExampleMatcher { return propertySpecifiers.values(); } } + + /** + * The match modes to expose so that clients can find about how to concatenate the predicates. + * + * @author Oliver Gierke + * @since 1.13 + * @see ExampleMatcher#isAllMatching() + */ + private static enum MatchMode { + ALL, ANY; + } } diff --git a/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java index 62cbefbf8..76e64f6dd 100644 --- a/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java +++ b/src/test/java/org/springframework/data/domain/ExampleMatcherUnitTests.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.ExampleMatcher.StringMatcher; * Unit test for {@link ExampleMatcher}. * * @author Mark Paluch + * @author Oliver Gierke * @soundtrack K2 - Der Berg Ruft (Club Mix) */ public class ExampleMatcherUnitTests { @@ -176,6 +177,36 @@ public class ExampleMatcherUnitTests { assertThat(configuredExampleSpec.isIgnoreCaseEnabled(), is(true)); } + /** + * @see DATACMNS-879 + */ + @Test + public void defaultMatcherRequiresAllMatching() { + + assertThat(matching().isAllMatching(), is(true)); + assertThat(matching().isAnyMatching(), is(false)); + } + + /** + * @see DATACMNS-879 + */ + @Test + public void allMatcherRequiresAllMatching() { + + assertThat(matchingAll().isAllMatching(), is(true)); + assertThat(matchingAll().isAnyMatching(), is(false)); + } + + /** + * @see DATACMNS-879 + */ + @Test + public void anyMatcherYieldsAnyMatching() { + + assertThat(matchingAny().isAnyMatching(), is(true)); + assertThat(matchingAny().isAllMatching(), is(false)); + } + static class Person { String firstname;