diff --git a/pom.xml b/pom.xml index 0710461991..32be87f991 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 1.12.0.BUILD-SNAPSHOT + 1.12.0.DATACMNS-810-SNAPSHOT Spring Data Core diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc new file mode 100644 index 0000000000..1c07fb3a38 --- /dev/null +++ b/src/main/asciidoc/query-by-example.adoc @@ -0,0 +1,213 @@ +[[query.by.example]] +== Query by Example + +=== Introduction + +This chapter will give you an introduction to Query by Example and explain how to use Examples. + +Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. + +=== Usage + +The Query by Example API consists of three parts: + +* Probe: That is the actual example of a domain object with populated fields. +* `ExampleSpec`: The `ExampleSpec` carries details on how to match particular fields. It can be reused across multiple Examples. `ExampleSpec` comes in two flavors: <> and <>. +* `Example`: An Example consists of the probe and the ExampleSpec. It is used to create the query. An `Example` takes a probe (usually the domain object or a subtype of it) and the `ExampleSpec`. + +Query by Example is suited for several use-cases but also comes with limitations: + +**When to use** + +* Querying your data store with a set of static or dynamic constraints +* Frequent refactoring of the domain objects without worrying about breaking existing queries +* Works independently from the underlying data store API + +**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 + +Before getting started with Query by Example, you need to have a domain object. To get started, simply create an interface for your repository: + +.Sample Person object +==== +[source,java] +---- +public class Person { + + @Id + private String id; + private String firstname; + private String lastname; + private Address address; + + // … getters and setters omitted +} +---- +==== + +This is a simple domain object. You can use it to create an `Example`. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `of` factory method or by using <>. `Example` is immutable. + +.Simple Example +==== +[source,java] +---- +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +Example example = Example.of(person); <3> +---- +<1> Create a new instance of the domain object +<2> Set the properties to query +<3> Create the `Example` +==== + +NOTE: Property names of the sample object must correlate with the property names of the queried domain object. + +Examples can be executed ideally with Repositories. To do so, let your repository extend from `QueryByExampleExecutor`, here's an excerpt from the `QueryByExampleExecutor` interface: + +.The `QueryByExampleExecutor` +==== +[source, java] +---- +public interface QueryByExampleExecutor { + + S findOne(Example example); + + Iterable findAll(Example example); + + // … more functionality omitted. +} +---- +==== + +You can read more about <> below. + +[[query.by.example.examplespec]] +=== Example Spec + +Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the `ExampleSpec`. `ExampleSpec` comes in two flavors: untyped and typed. By default `Example.of(Person.class)` uses an untyped `ExampleSpec`. Using untyped `ExampleSpec` will use the Repository entity information to determine the type to query and has no control over inheritance queries. Also, untyped `ExampleSpec` will use the probe type when using with a Template to determine the type to query. Read more about <> below. + +.Untyped Example Spec with customized matching +==== +[source,java] +---- +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +ExampleSpec exampleSpec = ExampleSpec.untyped() <3> + + .withIgnorePaths("lastname") <4> + + .withIncludeNullValues() <5> + + .withStringMatcherEnding(); <6> + +Example example = Example.of(person, exampleSpec); <7> + +---- +<1> Create a new instance of the domain object. +<2> Set properties. +<3> Create an untyped `ExampleSpec`. The `ExampleSpec` is usable at this stage. +<4> Construct a new `ExampleSpec` to ignore the property path `lastname`. +<5> Construct a new `ExampleSpec` to ignore the property path `lastname` and to include null values. +<6> Construct a new `ExampleSpec` 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 `ExampleSpec`. +==== + +`ExampleSpec` is immutable. Calls to `with…(…)` return a copy of `ExampleSpec` with the specific setting applied. Intermediate objects can be safely reused. An `ExampleSpec` can be used to create ad-hoc example specs or to be reused across the application as a specification for `Example`. You can use `ExampleSpec` as a template to configure a default behavior for your example spec. You also can derive from it a more specific `ExampleSpec` where you need to customize it. + +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 +==== +[source,java] +---- +ExampleSpec exampleSpec = ExampleSpec.untyped() + .withMatcher("firstname", endsWith()) + .withMatcher("lastname", startsWith().ignoreCase()); +} +---- +==== + +Another style to configure matcher options is by using Java 8 lambdas. This approach is a callback that asks the implementor to modify the matcher. It's not required to return the matcher because configuration options are held within the matcher instance. + +.Configuring matcher options with lambdas +==== +[source,java] +---- +ExampleSpec exampleSpec = ExampleSpec.untyped() + .withMatcher("firstname", matcher -> matcher.endsWith()) + .withMatcher("firstname", matcher -> matcher.startsWith()); +} +---- +==== + +Queries created by `Example` use a merged view of the configuration. Default matching settings can be set at `ExampleSpec` level while individual settings can be applied to particular property paths. Settings that are set on `ExampleSpec` are inherited by property path settings unless they are defined explicitly. Settings on a property patch have higher precedence than default settings. + +[cols="1,2", options="header"] +.Scope of `ExampleSpec` settings +|=== +| Setting +| Scope + +| Null-handling +| `ExampleSpec` + +| String matching +| `ExampleSpec` and property path + +| Ignoring properties +| Property path + +| Case sensitivity +| `ExampleSpec` and property path + +| Value transformation +| Property path + +|=== + +[[query.by.example.examplespec.typed]] +==== Typed Example Spec +You have now seen the usage of untyped `ExampleSpec`. The second flavor of `ExampleSpec` is typed which adds more control over the result type. When executing an `Example` containing a typed `ExampleSpec` the type of the `ExampleSpec` is used as domain type. Control over the domain type is useful in particular when querying along the inheritance hierarchy or the repository contains multiple types within one table/collection/keyspace. + +.Sample Person object +==== +[source,java] +---- +public class SpecialPerson extends Person { + + // … more functionality omitted. +} +---- +==== + +.Typed Example Spec with customized matching +==== +[source,java] +---- +QueryByExampleExecutor personRepository = … ; + +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +ExampleSpec exampleSpec = ExampleSpec.typed(SpecialPerson.class); <3> + +Example example = Example.of(person, exampleSpec); <4> + +List result = personRepository.findAll(example); <5> + +---- +<1> Create a new instance of the domain object. +<2> Set properties. +<3> Create a typed `ExampleSpec` for `SpecialPerson` that extends `Person`. +<4> Construct a new `Example` using the typed `ExampleSpec` and the `Person` probe. +<5> Run a query to select all instances of `SpecialPerson` in the repository. Note that the result type is the base class `Person`. +==== + diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java new file mode 100644 index 0000000000..3fa95c20e6 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -0,0 +1,140 @@ +/* + * Copyright 2016 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.domain; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Support for query by example (QBE). An {@link Example} takes a {@code probe} to define the example. Matching options + * and type safety can be tuned using {@link ExampleSpec}. {@link Example} uses {@link ExampleSpec#untyped()} + * {@link ExampleSpec} by default. + *

+ * This class is immutable. + * + * @author Christoph Strobl + * @author Mark Paluch + * @param + * @since 1.12 + */ +public class Example { + + private final T probe; + private final ExampleSpec exampleSpec; + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe The probe to use. Must not be {@literal null}. + */ + @SuppressWarnings("unchecked") + private Example(T probe) { + + Assert.notNull(probe, "Probe must not be null!"); + + this.probe = probe; + this.exampleSpec = ExampleSpec.untyped(); + } + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe The probe to use. Must not be {@literal null}. + * @param exampleSpec The example specification to use. Must not be {@literal null}. + */ + private Example(T probe, ExampleSpec exampleSpec) { + + Assert.notNull(probe, "Probe must not be null!"); + + this.probe = probe; + this.exampleSpec = exampleSpec; + } + + /** + * Create a new {@link Example} including all non-null properties by default using an untyped {@link ExampleSpec}. + * + * @param probe must not be {@literal null}. + * @return + */ + public static Example of(T probe) { + return new Example(probe); + } + + /** + * Create a new {@link Example} with a configured untyped {@link ExampleSpec}. + * + * @param probe must not be {@literal null}. + * @param exampleSpec must not be {@literal null}. + * @return + */ + public static Example of(T probe, ExampleSpec exampleSpec) { + return new Example(probe, exampleSpec); + } + + /** + * Create a new {@link Example} with a configured {@link TypedExampleSpec}. + * + * @param probe must not be {@literal null}. + * @param exampleSpec must not be {@literal null}. + * @return + */ + public static Example of(S probe, TypedExampleSpec exampleSpec) { + return new Example(probe, exampleSpec); + } + + /** + * Get the example used. + * + * @return never {@literal null}. + */ + public T getProbe() { + return probe; + } + + /** + * Get the {@link ExampleSpec} used. + * + * @return never {@literal null}. + */ + public ExampleSpec getExampleSpec() { + return exampleSpec; + } + + /** + * Get the actual type for the probe used. This is usually the given class, but the original class in case of a + * CGLIB-generated subclass. + * + * @return + * @see ClassUtils#getUserClass(Class) + */ + @SuppressWarnings("unchecked") + public Class getProbeType() { + return (Class) ClassUtils.getUserClass(probe.getClass()); + } + + /** + * Get the actual result type for the query. The result type can be different when using {@link TypedExampleSpec}. + * + * @return + * @see ClassUtils#getUserClass(Class) + */ + @SuppressWarnings("unchecked") + public Class getResultType() { + return (Class) (exampleSpec instanceof TypedExampleSpec ? ((TypedExampleSpec) exampleSpec).getType() + : getProbeType()); + } + +} diff --git a/src/main/java/org/springframework/data/domain/ExampleSpec.java b/src/main/java/org/springframework/data/domain/ExampleSpec.java new file mode 100644 index 0000000000..9d8051fabc --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ExampleSpec.java @@ -0,0 +1,845 @@ +/* + * Copyright 2016 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.domain; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.Assert; + +/** + * Specification for property path matching to use in query by example (QBE). An {@link ExampleSpec} can be created for + * a {@link Class object type}. Instances of {@link ExampleSpec} can be either {@link #untyped()} or + * {@link #typed(Class)} and settings can be tuned {@code with...} methods in a fluent style. {@code with...} methods + * return a copy of the {@link ExampleSpec} instance with the specified setting. Null-handling defaults to + * {@link NullHandler#IGNORE} and case-sensitive {@link StringMatcher#DEFAULT} string matching. + *

+ * This class is immutable. + * + * @author Christoph Strobl + * @author Mark Paluch + * @param + * @since 1.12 + */ +public class ExampleSpec { + + protected final NullHandler nullHandler; + protected final StringMatcher defaultStringMatcher; + protected final boolean defaultIgnoreCase; + protected final PropertySpecifiers propertySpecifiers; + protected final Set ignoredPaths; + + ExampleSpec() { + + this.nullHandler = NullHandler.IGNORE; + this.defaultStringMatcher = StringMatcher.DEFAULT; + this.propertySpecifiers = new PropertySpecifiers(); + this.defaultIgnoreCase = false; + this.ignoredPaths = Collections.emptySet(); + } + + ExampleSpec(NullHandler nullHandler, StringMatcher defaultStringMatcher, PropertySpecifiers propertySpecifiers, + Set ignoredPaths, boolean defaultIgnoreCase) { + + this.nullHandler = nullHandler; + this.defaultStringMatcher = defaultStringMatcher; + this.propertySpecifiers = propertySpecifiers; + this.ignoredPaths = Collections.unmodifiableSet(ignoredPaths); + this.defaultIgnoreCase = defaultIgnoreCase; + } + + /** + * Create a new untyped {@link ExampleSpec} including all non-null properties by default. + * + * @param type must not be {@literal null}. + * @return + */ + public static ExampleSpec untyped() { + return new ExampleSpec(); + } + + /** + * Create a new {@link TypedExampleSpec} including all non-null properties by default. + * + * @param type must not be {@literal null}. + * @return + */ + public static TypedExampleSpec typed(Class type) { + return new TypedExampleSpec(type); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified {@code propertyPaths}. This instance is immutable and + * unaffected by this method call. + * + * @param ignoredPaths must not be {@literal null} and not empty. + * @return + */ + public ExampleSpec withIgnorePaths(String... ignoredPaths) { + + Assert.notEmpty(ignoredPaths, "IgnoredPaths must not be empty!"); + Assert.noNullElements(ignoredPaths, "IgnoredPaths must not contain null elements!"); + + Set newIgnoredPaths = new LinkedHashSet(this.ignoredPaths); + newIgnoredPaths.addAll(Arrays.asList(ignoredPaths)); + + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified string matching of {@link StringMatcher#STARTING}. + * This instance is immutable and unaffected by this method call. + * + * @return + */ + public ExampleSpec withStringMatcherStarting() { + return new ExampleSpec(nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified string matching of {@link StringMatcher#ENDING}. This + * instance is immutable and unaffected by this method call. + * + * @return + */ + public ExampleSpec withStringMatcherEnding() { + return new ExampleSpec(nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified string matching of {@link StringMatcher#CONTAINING}. + * This instance is immutable and unaffected by this method call. + * + * @return + */ + public ExampleSpec withStringMatcherContaining() { + return new ExampleSpec(nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified string matching of {@code defaultStringMatcher}. This + * instance is immutable and unaffected by this method call. + * + * @param defaultStringMatcher must not be {@literal null}. + * @return + */ + public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { + + Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); + + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with ignoring case sensitivity by default. This instance is immutable + * and unaffected by this method call. + * + * @return + */ + public ExampleSpec withIgnoreCase() { + return withIgnoreCase(true); + } + + /** + * Returns a copy of this {@link ExampleSpec} with {@code defaultIgnoreCase}. This instance is immutable and + * unaffected by this method call. + * + * @param defaultIgnoreCase + * @return + */ + public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified {@code GenericPropertyMatcher} for the + * {@code propertyPath}. This instance is immutable and unaffected by this method call. + * + * @param propertyPath must not be {@literal null}. + * @param matcherConfigurer callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}. + * @return + */ + public ExampleSpec withMatcher(String propertyPath, MatcherConfigurer matcherConfigurer) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(matcherConfigurer, "MatcherConfigurer must not be empty!"); + + GenericPropertyMatcher genericPropertyMatcher = new GenericPropertyMatcher(); + matcherConfigurer.configureMatcher(genericPropertyMatcher); + + return withMatcher(propertyPath, genericPropertyMatcher); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified {@code GenericPropertyMatcher} for the + * {@code propertyPath}. This instance is immutable and unaffected by this method call. + * + * @param propertyPath must not be {@literal null}. + * @param genericPropertyMatcher callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}. + * @return + */ + public ExampleSpec withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(genericPropertyMatcher, "GenericPropertyMatcher must not be empty!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + PropertySpecifier propertySpecifier = new PropertySpecifier(propertyPath); + + if (genericPropertyMatcher.ignoreCase != null) { + propertySpecifier = propertySpecifier.withIgnoreCase(genericPropertyMatcher.ignoreCase); + } + + if (genericPropertyMatcher.stringMatcher != null) { + propertySpecifier = propertySpecifier.withStringMatcher(genericPropertyMatcher.stringMatcher); + } + + if (genericPropertyMatcher.valueTransformer != null) { + propertySpecifier = propertySpecifier.withValueTransformer(genericPropertyMatcher.valueTransformer); + } + + propertySpecifiers.add(propertySpecifier); + + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified {@code PropertyValueTransformer} for the + * {@code propertyPath}. + * + * @param propertyPath must not be {@literal null}. + * @param propertyValueTransformer must not be {@literal null}. + * @return + */ + public ExampleSpec withTransformer(String propertyPath, PropertyValueTransformer propertyValueTransformer) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(propertyValueTransformer, "PropertyValueTransformer must not be empty!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); + + propertySpecifiers.add(propertySpecifier.withValueTransformer(propertyValueTransformer)); + + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with ignore case sensitivity for the {@code propertyPaths}. This + * instance is immutable and unaffected by this method call. + * + * @param propertyPaths must not be {@literal null} and not empty. + * @return + */ + public ExampleSpec withIgnoreCase(String... propertyPaths) { + + Assert.notEmpty(propertyPaths, "PropertyPaths must not be empty!"); + Assert.noNullElements(propertyPaths, "PropertyPaths must not contain null elements!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + + for (String propertyPath : propertyPaths) { + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); + propertySpecifiers.add(propertySpecifier.withIgnoreCase(true)); + } + + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + protected PropertySpecifier getOrCreatePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { + + if (propertySpecifiers.hasSpecifierForPath(propertyPath)) { + return propertySpecifiers.getForPath(propertyPath); + } + + return new PropertySpecifier(propertyPath); + } + + /** + * Returns a copy of this {@link ExampleSpec} with treatment for {@literal null} values of {@link NullHandler#INCLUDE} + * . This instance is immutable and unaffected by this method call. + * + * @return + */ + public ExampleSpec withIncludeNullValues() { + return new ExampleSpec(NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with treatment for {@literal null} values of {@link NullHandler#IGNORE}. + * This instance is immutable and unaffected by this method call. + * + * @return + */ + public ExampleSpec withIgnoreNullValues() { + return new ExampleSpec(NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link ExampleSpec} with the specified {@code nullHandler}. This instance is immutable and + * unaffected by this method call. + * + * @param nullHandler must not be {@literal null}. + * @return + */ + public ExampleSpec withNullHandler(NullHandler nullHandler) { + + Assert.notNull(nullHandler, "NullHandler must not be null!"); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + } + + /** + * Get defined null handling. + * + * @return never {@literal null} + */ + public ExampleSpec.NullHandler getNullHandler() { + return nullHandler; + } + + /** + * Get defined {@link ExampleSpec.StringMatcher}. + * + * @return never {@literal null}. + */ + public ExampleSpec.StringMatcher getDefaultStringMatcher() { + return defaultStringMatcher; + } + + /** + * @return {@literal true} if {@link String} should be matched with ignore case option. + */ + public boolean isIgnoreCaseEnabled() { + return this.defaultIgnoreCase; + } + + /** + * @param path + * @return return {@literal true} if path was set to be ignored. + */ + public boolean isIgnoredPath(String path) { + return this.ignoredPaths.contains(path); + } + + /** + * @return unmodifiable {@link Set} of ignored paths. + */ + public Set getIgnoredPaths() { + return ignoredPaths; + } + + /** + * @return the {@link PropertySpecifiers} within the {@link ExampleSpec}. + */ + public PropertySpecifiers getPropertySpecifiers() { + return propertySpecifiers; + } + + /** + * Null handling for creating criterion out of an {@link Example}. + * + * @author Christoph Strobl + */ + public static enum NullHandler { + + INCLUDE, IGNORE + } + + /** + * Callback to configure a matcher. + * + * @author Mark Paluch + * @param + */ + public static interface MatcherConfigurer { + void configureMatcher(T matcher); + } + + /** + * A generic property matcher that specifies {@link StringMatcher string matching} and case sensitivity. + * + * @author Mark Paluch + */ + public static class GenericPropertyMatcher { + + StringMatcher stringMatcher = null; + Boolean ignoreCase = null; + PropertyValueTransformer valueTransformer = NoOpPropertyValueTransformer.INSTANCE; + + /** + * Creates an unconfigured {@link GenericPropertyMatcher}. + */ + public GenericPropertyMatcher() {} + + /** + * Creates a new {@link GenericPropertyMatcher} with a {@link StringMatcher} and {@code ignoreCase}. + * + * @param stringMatcher must not be {@literal null}. + * @param ignoreCase + * @return + */ + public static GenericPropertyMatcher of(StringMatcher stringMatcher, boolean ignoreCase) { + return new GenericPropertyMatcher().stringMatcher(stringMatcher).ignoreCase(ignoreCase); + } + + /** + * Creates a new {@link GenericPropertyMatcher} with a {@link StringMatcher} and {@code ignoreCase}. + * + * @param stringMatcher must not be {@literal null}. + * @return + */ + public static GenericPropertyMatcher of(StringMatcher stringMatcher) { + return new GenericPropertyMatcher().stringMatcher(stringMatcher); + } + + /** + * Sets ignores case to {@literal true}. + * + * @return + */ + public GenericPropertyMatcher ignoreCase() { + + this.ignoreCase = true; + return this; + } + + /** + * Sets ignores case to {@code ignoreCase}. + * + * @param ignoreCase + * @return + */ + public GenericPropertyMatcher ignoreCase(boolean ignoreCase) { + + this.ignoreCase = ignoreCase; + return this; + } + + /** + * Sets ignores case to {@literal false}. + * + * @return + */ + public GenericPropertyMatcher caseSensitive() { + + this.ignoreCase = false; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#CONTAINING}. + * + * @return + */ + public GenericPropertyMatcher contains() { + + this.stringMatcher = StringMatcher.CONTAINING; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#ENDING}. + * + * @return + */ + public GenericPropertyMatcher endsWith() { + + this.stringMatcher = StringMatcher.ENDING; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#STARTING}. + * + * @return + */ + public GenericPropertyMatcher startsWith() { + + this.stringMatcher = StringMatcher.STARTING; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#EXACT}. + * + * @return + */ + public GenericPropertyMatcher exact() { + + this.stringMatcher = StringMatcher.EXACT; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#DEFAULT}. + * + * @return + */ + public GenericPropertyMatcher storeDefaultMatching() { + + this.stringMatcher = StringMatcher.DEFAULT; + return this; + } + + /** + * Sets string matcher to {@link StringMatcher#REGEX}. + * + * @return + */ + public GenericPropertyMatcher regex() { + + this.stringMatcher = StringMatcher.REGEX; + return this; + } + + /** + * Sets string matcher to {@code stringMatcher}. + * + * @param stringMatcher must not be {@literal null}. + * @return + */ + public GenericPropertyMatcher stringMatcher(StringMatcher stringMatcher) { + + Assert.notNull(stringMatcher, "StringMatcher must not be null!"); + this.stringMatcher = stringMatcher; + return this; + } + + /** + * Sets the {@link PropertyValueTransformer} to {@code propertyValueTransformer}. + * + * @param propertyValueTransformer must not be {@literal null}. + * @return + */ + public GenericPropertyMatcher transform(PropertyValueTransformer propertyValueTransformer) { + + Assert.notNull(propertyValueTransformer, "PropertyValueTransformer must not be null!"); + this.valueTransformer = propertyValueTransformer; + return this; + } + + } + + /** + * Predefined property matchers to create a {@link GenericPropertyMatcher}. + * + * @author Mark Paluch + */ + public static class GenericPropertyMatchers { + + /** + * Creates a {@link GenericPropertyMatcher} that matches string case insensitive. + * + * @return + */ + public static GenericPropertyMatcher ignoreCase() { + return new GenericPropertyMatcher().ignoreCase(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string case sensitive. + * + * @return + */ + public static GenericPropertyMatcher caseSensitive() { + return new GenericPropertyMatcher().caseSensitive(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#CONTAINING}. + * + * @return + */ + public static GenericPropertyMatcher contains() { + return new GenericPropertyMatcher().contains(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#ENDING}. + * + * @return + */ + public static GenericPropertyMatcher endsWith() { + return new GenericPropertyMatcher().endsWith(); + + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#STARTING}. + * + * @return + */ + public static GenericPropertyMatcher startsWith() { + return new GenericPropertyMatcher().startsWith(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#EXACT}. + * + * @return + */ + public static GenericPropertyMatcher exact() { + return new GenericPropertyMatcher().startsWith(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#DEFAULT}. + * + * @return + */ + public static GenericPropertyMatcher storeDefaultMatching() { + return new GenericPropertyMatcher().storeDefaultMatching(); + } + + /** + * Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#REGEX}. + * + * @return + */ + public static GenericPropertyMatcher regex() { + return new GenericPropertyMatcher().regex(); + } + + } + + /** + * Match modes for treatment of {@link String} values. + * + * @author Christoph Strobl + */ + public static enum StringMatcher { + + /** + * Store specific default. + */ + DEFAULT, + /** + * Matches the exact string + */ + EXACT, + /** + * Matches string starting with pattern + */ + STARTING, + /** + * Matches string ending with pattern + */ + ENDING, + /** + * Matches string containing pattern + */ + CONTAINING, + /** + * Treats strings as regular expression patterns + */ + REGEX; + + } + + /** + * Allows to transform the property value before it is used in the query. + */ + public static interface PropertyValueTransformer extends Converter { + // TODO: should we use the converter interface directly or not at all? + } + + /** + * @author Christoph Strobl + * @since 1.12 + */ + public static enum NoOpPropertyValueTransformer implements ExampleSpec.PropertyValueTransformer { + + INSTANCE; + + @Override + public Object convert(Object source) { + return source; + } + + } + + /** + * Define specific property handling for a Dot-Path. + * + * @author Christoph Strobl + * @since 1.12 + */ + public static class PropertySpecifier { + + private final String path; + + private final StringMatcher stringMatcher; + private final Boolean ignoreCase; + private final PropertyValueTransformer valueTransformer; + + /** + * Creates new {@link PropertySpecifier} for given path. + * + * @param path Dot-Path to the property. Must not be {@literal null}. + */ + PropertySpecifier(String path) { + + Assert.hasText(path, "Path must not be null/empty!"); + this.path = path; + + this.stringMatcher = null; + this.ignoreCase = null; + this.valueTransformer = NoOpPropertyValueTransformer.INSTANCE; + } + + private PropertySpecifier(String path, StringMatcher stringMatcher, Boolean ignoreCase, + PropertyValueTransformer valueTransformer) { + + this.path = path; + this.stringMatcher = stringMatcher; + this.ignoreCase = ignoreCase; + this.valueTransformer = valueTransformer; + } + + /** + * Creates a new {@link PropertySpecifier} containing all values from the current instance and sets + * {@link StringMatcher} in the returned instance. + * + * @param stringMatcher must not be {@literal null}. + * @return + */ + public PropertySpecifier withStringMatcher(StringMatcher stringMatcher) { + + Assert.notNull(stringMatcher, "StringMatcher must not be null!"); + return new PropertySpecifier(this.path, stringMatcher, this.ignoreCase, this.valueTransformer); + } + + /** + * Creates a new {@link PropertySpecifier} containing all values from the current instance and sets + * {@code ignoreCase}. + * + * @param ignoreCase must not be {@literal null}. + * @return + */ + public PropertySpecifier withIgnoreCase(boolean ignoreCase) { + return new PropertySpecifier(this.path, this.stringMatcher, ignoreCase, this.valueTransformer); + } + + /** + * Creates a new {@link PropertySpecifier} containing all values from the current instance and sets + * {@link PropertyValueTransformer} in the returned instance. + * + * @param valueTransformer must not be {@literal null}. + * @return + */ + public PropertySpecifier withValueTransformer(PropertyValueTransformer valueTransformer) { + + Assert.notNull(valueTransformer, "PropertyValueTransformer must not be null!"); + return new PropertySpecifier(this.path, this.stringMatcher, this.ignoreCase, valueTransformer); + } + + /** + * Get the properties Dot-Path. + * + * @return never {@literal null}. + */ + public String getPath() { + return path; + } + + /** + * Get the {@link StringMatcher}. + * + * @return can be {@literal null}. + */ + public StringMatcher getStringMatcher() { + return stringMatcher; + } + + /** + * @return {@literal null} if not set. + */ + public Boolean getIgnoreCase() { + return ignoreCase; + } + + /** + * Get the property transformer to be applied. + * + * @return never {@literal null}. + */ + public PropertyValueTransformer getPropertyValueTransformer() { + return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; + } + + /** + * Transforms a given source using the {@link PropertyValueTransformer}. + * + * @param source + * @return + */ + public Object transformValue(Object source) { + return getPropertyValueTransformer().convert(source); + } + + } + + /** + * Define specific property handling for Dot-Paths. + * + * @author Christoph Strobl + * @since 1.12 + */ + public static class PropertySpecifiers { + + private final Map propertySpecifiers = new LinkedHashMap(); + + PropertySpecifiers() { + + } + + PropertySpecifiers(PropertySpecifiers propertySpecifiers) { + this.propertySpecifiers.putAll(propertySpecifiers.propertySpecifiers); + } + + public void add(PropertySpecifier specifier) { + + Assert.notNull(specifier, "PropertySpecifier must not be null!"); + propertySpecifiers.put(specifier.getPath(), specifier); + } + + public boolean hasSpecifierForPath(String path) { + return propertySpecifiers.containsKey(path); + } + + public PropertySpecifier getForPath(String path) { + return propertySpecifiers.get(path); + } + + public boolean hasValues() { + return !propertySpecifiers.isEmpty(); + } + + public Collection getSpecifiers() { + return propertySpecifiers.values(); + } + + } + +} diff --git a/src/main/java/org/springframework/data/domain/TypedExampleSpec.java b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java new file mode 100644 index 0000000000..1fb17b19ad --- /dev/null +++ b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java @@ -0,0 +1,313 @@ +/* + * Copyright 2016 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.domain; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A {@code TypedExampleSpec} is a special {@link ExampleSpec} that holds information of the type to query for. + * + * @author Mark Paluch + * @since 1.12 + */ +public class TypedExampleSpec extends ExampleSpec { + + protected final Class type; + + TypedExampleSpec(Class type) { + super(); + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + } + + TypedExampleSpec(Class type, NullHandler nullHandler, StringMatcher defaultStringMatcher, + PropertySpecifiers propertySpecifiers, Set ignoredPaths, boolean defaultIgnoreCase) { + + super(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); + + Assert.notNull(type, "Type must not be null!"); + this.type = type; + } + + /** + * Create a new {@link TypedExampleSpec} for the {@code type} including all non-null properties by default. + * + * @param type must not be {@literal null}. + * @return + */ + public static TypedExampleSpec of(Class type) { + return new TypedExampleSpec(type); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified {@code propertyPaths}. This instance is + * immutable and unaffected by this method call. + * + * @param ignoredPaths must not be {@literal null} and not empty. + * @return + */ + @Override + public TypedExampleSpec withIgnorePaths(String... ignoredPaths) { + + Assert.notEmpty(ignoredPaths, "IgnoredPaths must not be empty!"); + Assert.noNullElements(ignoredPaths, "IgnoredPaths must not contain null elements!"); + + Set newIgnoredPaths = new LinkedHashSet(this.ignoredPaths); + newIgnoredPaths.addAll(Arrays.asList(ignoredPaths)); + + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified string matching of + * {@link StringMatcher#STARTING}. This instance is immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherStarting() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified string matching of {@link StringMatcher#ENDING}. + * This instance is immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherEnding() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified string matching of + * {@link StringMatcher#CONTAINING}. This instance is immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherContaining() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified string matching of {@code defaultStringMatcher}. + * This instance is immutable and unaffected by this method call. + * + * @param defaultStringMatcher must not be {@literal null}. + * @return + */ + @Override + public TypedExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { + + Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); + + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with ignoring case sensitivity by default. This instance is + * immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withIgnoreCase() { + return withIgnoreCase(true); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with {@code defaultIgnoreCase}. This instance is immutable and + * unaffected by this method call. + * + * @param defaultIgnoreCase + * @return + */ + @Override + public TypedExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified {@code GenericPropertyMatcher} for the + * {@code propertyPath}. This instance is immutable and unaffected by this method call. + * + * @param propertyPath must not be {@literal null}. + * @param matcherConfigurer callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}. + * @return + */ + @Override + public TypedExampleSpec withMatcher(String propertyPath, + MatcherConfigurer matcherConfigurer) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(matcherConfigurer, "MatcherConfigurer must not be empty!"); + + GenericPropertyMatcher genericPropertyMatcher = new GenericPropertyMatcher(); + matcherConfigurer.configureMatcher(genericPropertyMatcher); + + return withMatcher(propertyPath, genericPropertyMatcher); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified {@code GenericPropertyMatcher} for the + * {@code propertyPath}. This instance is immutable and unaffected by this method call. + * + * @param propertyPath must not be {@literal null}. + * @param genericPropertyMatcher callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}. + * @return + */ + @Override + public TypedExampleSpec withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(genericPropertyMatcher, "GenericPropertyMatcher must not be empty!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + PropertySpecifier propertySpecifier = new PropertySpecifier(propertyPath); + + if (genericPropertyMatcher.ignoreCase != null) { + propertySpecifier = propertySpecifier.withIgnoreCase(genericPropertyMatcher.ignoreCase); + } + + if (genericPropertyMatcher.stringMatcher != null) { + propertySpecifier = propertySpecifier.withStringMatcher(genericPropertyMatcher.stringMatcher); + } + + if (genericPropertyMatcher.valueTransformer != null) { + propertySpecifier = propertySpecifier.withValueTransformer(genericPropertyMatcher.valueTransformer); + } + + propertySpecifiers.add(propertySpecifier); + + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified {@code PropertyValueTransformer} for the + * {@code propertyPath}. + * + * @param propertyPath must not be {@literal null}. + * @param propertyValueTransformer must not be {@literal null}. + * @return + */ + @Override + public TypedExampleSpec withTransformer(String propertyPath, PropertyValueTransformer propertyValueTransformer) { + + Assert.hasText(propertyPath, "PropertyPath must not be empty!"); + Assert.notNull(propertyValueTransformer, "PropertyValueTransformer must not be empty!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); + + propertySpecifiers.add(propertySpecifier.withValueTransformer(propertyValueTransformer)); + + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with ignore case sensitivity for the {@code propertyPaths}. This + * instance is immutable and unaffected by this method call. + * + * @param propertyPaths must not be {@literal null} and not empty. + * @return + */ + @Override + public TypedExampleSpec withIgnoreCase(String... propertyPaths) { + + Assert.notEmpty(propertyPaths, "PropertyPaths must not be empty!"); + Assert.noNullElements(propertyPaths, "PropertyPaths must not contain null elements!"); + + PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); + + for (String propertyPath : propertyPaths) { + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); + propertySpecifiers.add(propertySpecifier.withIgnoreCase(true)); + } + + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with treatment for {@literal null} values of + * {@link NullHandler#INCLUDE} . This instance is immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withIncludeNullValues() { + return new TypedExampleSpec(type, NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with treatment for {@literal null} values of + * {@link NullHandler#IGNORE}. This instance is immutable and unaffected by this method call. + * + * @return + */ + @Override + public TypedExampleSpec withIgnoreNullValues() { + return new TypedExampleSpec(type, NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Returns a copy of this {@link TypedExampleSpec} with the specified {@code nullHandler}. This instance is immutable + * and unaffected by this method call. + * + * @param nullHandler must not be {@literal null}. + * @return + */ + @Override + public TypedExampleSpec withNullHandler(NullHandler nullHandler) { + + Assert.notNull(nullHandler, "NullHandler must not be null!"); + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Get the actual type for the {@link TypedExampleSpec} used. This is usually the given class, but the original class + * in case of a CGLIB-generated subclass. + * + * @return + * @see ClassUtils#getUserClass(Class) + */ + @SuppressWarnings("unchecked") + public Class getType() { + return (Class) ClassUtils.getUserClass(type); + } + +} diff --git a/src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java b/src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java new file mode 100644 index 0000000000..3afc2b90f0 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java @@ -0,0 +1,159 @@ +/* + * Copyright 2016 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.repository.core.support; + +import java.util.Collection; + +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.TypedExampleSpec; + +/** + * Accessor for the {@link ExampleSpec} to use in modules that support query by example (QBE) querying. + * + * @author Mark Paluch + * @since 1.12 + */ +public class ExampleSpecAccessor { + + private final ExampleSpec exampleSpec; + + public ExampleSpecAccessor(ExampleSpec exampleSpec) { + this.exampleSpec = exampleSpec; + } + + /** + * @return unmodifiable {@link Collection} of {@link ExampleSpec.PropertySpecifier}s. + */ + public Collection getPropertySpecifiers() { + return exampleSpec.getPropertySpecifiers().getSpecifiers(); + } + + /** + * @param path Dot-Path to property. + * @return {@literal true} in case {@link ExampleSpec.PropertySpecifier} defined for given path. + */ + public boolean hasPropertySpecifier(String path) { + return exampleSpec.getPropertySpecifiers().hasSpecifierForPath(path); + } + + /** + * Get the {@link ExampleSpec.PropertySpecifier} for given path.
+ * Please check if {@link #hasPropertySpecifier(String)} to avoid running into {@literal null} values. + * + * @param path Dot-Path to property. + * @return {@literal null} when no {@link ExampleSpec.PropertySpecifier} defined for path. + */ + public ExampleSpec.PropertySpecifier getPropertySpecifier(String path) { + return exampleSpec.getPropertySpecifiers().getForPath(path); + } + + /** + * @return true if at least one {@link ExampleSpec.PropertySpecifier} defined. + */ + public boolean hasPropertySpecifiers() { + return exampleSpec.getPropertySpecifiers().hasValues(); + } + + /** + * Get the {@link ExampleSpec.StringMatcher} for a given path or return the default one if none defined. + * + * @param path + * @return never {@literal null}. + */ + public ExampleSpec.StringMatcher getStringMatcherForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return exampleSpec.getDefaultStringMatcher(); + } + + ExampleSpec.PropertySpecifier specifier = getPropertySpecifier(path); + return specifier.getStringMatcher() != null ? specifier.getStringMatcher() : exampleSpec.getDefaultStringMatcher(); + } + + /** + * Get defined null handling. + * + * @return never {@literal null} + */ + public ExampleSpec.NullHandler getNullHandler() { + return exampleSpec.getNullHandler(); + } + + /** + * Get defined {@link ExampleSpec.StringMatcher}. + * + * @return never {@literal null}. + */ + public ExampleSpec.StringMatcher getDefaultStringMatcher() { + return exampleSpec.getDefaultStringMatcher(); + } + + /** + * @return {@literal true} if {@link String} should be matched with ignore case option. + */ + public boolean isIgnoreCaseEnabled() { + return exampleSpec.isIgnoreCaseEnabled(); + } + + /** + * @param path + * @return return {@literal true} if path was set to be ignored. + */ + public boolean isIgnoredPath(String path) { + return exampleSpec.isIgnoredPath(path); + } + + /** + * Get the ignore case flag for a given path or return the default one if none defined. + * + * @param path + * @return never {@literal null}. + */ + public boolean isIgnoreCaseForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return exampleSpec.isIgnoreCaseEnabled(); + } + + ExampleSpec.PropertySpecifier specifier = getPropertySpecifier(path); + return specifier.getIgnoreCase() != null ? specifier.getIgnoreCase().booleanValue() + : exampleSpec.isIgnoreCaseEnabled(); + } + + /** + * Get the ignore case flag for a given path or return {@link ExampleSpec.NoOpPropertyValueTransformer} if none + * defined. + * + * @param path + * @return never {@literal null}. + */ + public ExampleSpec.PropertyValueTransformer getValueTransformerForPath(String path) { + + if (!hasPropertySpecifier(path)) { + return ExampleSpec.NoOpPropertyValueTransformer.INSTANCE; + } + + return getPropertySpecifier(path).getPropertyValueTransformer(); + } + + /** + * @return {@literal true} if the {@link ExampleSpec} is typed. + */ + public boolean isTyped() { + return exampleSpec instanceof TypedExampleSpec; + } + +} diff --git a/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java b/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java new file mode 100644 index 0000000000..a5421e27f2 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016 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.repository.query; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * Interface to allow execution of Query by Example {@link Example} instances. + * + * @param + * @author Mark Paluch + * @since 1.12 + */ +public interface QueryByExampleExecutor { + + /** + * Returns a single entity matching the given {@link Example} or {@literal null} if none was found. + * + * @param example can be {@literal null}. + * @return a single entity matching the given {@link Example} or {@literal null} if none was found. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the Example yields more than one result. + */ + S findOne(Example example); + + /** + * Returns all entities matching the given {@link Example}. In case no match could be found an empty {@link Iterable} + * is returned. + * + * @param example can be {@literal null}. + * @return all entities matching the given {@link Example}. + */ + Iterable findAll(Example example); + + /** + * Returns all entities matching the given {@link Example} applying the given {@link Sort}. In case no match could be + * found an empty {@link Iterable} is returned. + * + * @param example can be {@literal null}. + * @param sort the {@link Sort} specification to sort the results by, must not be {@literal null}. + * @return all entities matching the given {@link Example}. + * @since 1.10 + */ + Iterable findAll(Example example, Sort sort); + + /** + * Returns a {@link Page} of entities matching the given {@link Example}. In case no match could be found, an empty + * {@link Page} is returned. + * + * @param example can be {@literal null}. + * @param pageable can be {@literal null}. + * @return a {@link Page} of entities matching the given {@link Example}. + */ + Page findAll(Example example, Pageable pageable); + + /** + * Returns the number of instances matching the given {@link Example}. + * + * @param example the {@link Example} to count instances for, can be {@literal null}. + * @return the number of instances matching the {@link Example}. + */ + long count(Example example); + + /** + * Checks whether the data store contains elements that match the given {@link Example}. + * + * @param example the {@link Example} to use for the existence check, can be {@literal null}. + * @return {@literal true} if the data store contains elements that match the given {@link Example}. + */ + boolean exists(Example example); +} diff --git a/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java new file mode 100644 index 0000000000..fbd2fcca3b --- /dev/null +++ b/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2016 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.domain; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.StringMatcher; + +/** + * Unit test for {@link ExampleSpec}. + * + * @author Mark Paluch + * @soundtrack K2 - Der Berg Ruft (Club Mix) + */ +public class ExampleSpecUnitTests { + + ExampleSpec exampleSpec; + + @Before + public void setUp() throws Exception { + exampleSpec = ExampleSpec.untyped(); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() throws Exception { + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnContainingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.untyped().withStringMatcherContaining(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.untyped().withStringMatcherStarting(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnEndingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.untyped().withStringMatcherEnding(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() throws Exception { + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsIsEmptyByDefault() throws Exception { + assertThat(exampleSpec.getIgnoredPaths(), is(empty())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnoreByDefault() throws Exception { + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = UnsupportedOperationException.class) + public void ignoredPathsIsNotModifiable() throws Exception { + exampleSpec.getIgnoredPaths().add("¯\\_(ツ)_/¯"); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = IllegalArgumentException.class) + public void defaultExampleSpecWithoutTypeFails() throws Exception { + ExampleSpec.typed(null); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase(); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase(true); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIncludeNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnore() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnoreNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnConfiguredValue() throws Exception { + + exampleSpec = ExampleSpec.untyped().withNullHandler(NullHandler.INCLUDE); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnCorrectProperties() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "baz"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar", "baz")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(3)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnUniqueProperties() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "foo"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void withCreatesNewInstance() throws Exception { + + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "foo"); + ExampleSpec configuredExampleSpec = exampleSpec.withIgnoreCase(); + + assertThat(exampleSpec, is(not(sameInstance(configuredExampleSpec)))); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + + assertThat(configuredExampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(configuredExampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + static class Person { + + String firstname; + } +} diff --git a/src/test/java/org/springframework/data/domain/ExampleUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java new file mode 100644 index 0000000000..8cff9970e6 --- /dev/null +++ b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016 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.domain; + +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.springframework.data.domain.Example.*; + +import org.junit.Before; +import org.junit.Test; + +/** + * Test for {@link Example}. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +public class ExampleUnitTests { + + private Person person; + private Example example; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + example = of(person); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = IllegalArgumentException.class) + public void exampleOfNullThrowsException() { + Example.of(null); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getSampleTypeRetunsSampleObjectsClass() { + assertThat(example.getProbeType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void createTypedExample() { + + Example example = of(new Person(), ExampleSpec.typed(Contact.class)); + + assertThat(example.getProbeType(), equalTo(Person.class)); + assertThat(example.getResultType(), equalTo((Class) Contact.class)); + assertThat(example.getProbeType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void createUntypedExample() { + + Example example = of(new Person(), ExampleSpec.untyped()); + + assertThat(example.getProbeType(), equalTo(Person.class)); + assertThat(example.getResultType(), equalTo((Class) Person.class)); + assertThat(example.getProbeType(), equalTo(Person.class)); + } + + static class Contact { + + String label; + } + + static class Person { + + String firstname; + } + +} diff --git a/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java b/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java new file mode 100644 index 0000000000..aea31209ad --- /dev/null +++ b/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java @@ -0,0 +1,231 @@ +/* + * Copyright 2016 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.domain; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.StringMatcher; + +/** + * Unit test for {@link ExampleSpec}. + * + * @author Mark Paluch + * @soundtrack Harmonic Rush - The Dark Side of Persia (ahmed romel remix) + */ +public class TypedExampleSpecUnitTests { + + TypedExampleSpec exampleSpec; + + @Before + public void setUp() throws Exception { + exampleSpec = ExampleSpec.typed(Person.class); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void typeShouldReturnConfiguredType() throws Exception { + assertThat(exampleSpec.getType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() throws Exception { + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnContainingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherContaining(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherStarting(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnEndingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherEnding(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() throws Exception { + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsIsEmptyByDefault() throws Exception { + assertThat(exampleSpec.getIgnoredPaths(), is(empty())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnoreByDefault() throws Exception { + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = UnsupportedOperationException.class) + public void ignoredPathsIsNotModifiable() throws Exception { + exampleSpec.getIgnoredPaths().add("¯\\_(ツ)_/¯"); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = IllegalArgumentException.class) + public void defaultExampleSpecWithoutTypeFails() throws Exception { + ExampleSpec.typed(null); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(true); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIncludeNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnore() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnConfiguredValue() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withNullHandler(NullHandler.INCLUDE); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnCorrectProperties() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "baz"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar", "baz")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(3)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnUniqueProperties() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "foo"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void withCreatesNewInstance() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "foo"); + TypedExampleSpec configuredExampleSpec = exampleSpec.withIgnoreCase(); + + assertThat(exampleSpec, is(not(sameInstance(configuredExampleSpec)))); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + + assertThat(configuredExampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(configuredExampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + static class Person { + + String firstname; + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java new file mode 100644 index 0000000000..195d6fb22a --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java @@ -0,0 +1,315 @@ +/* + * Copyright 2016 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.repository.core.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatcher; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatchers; +import org.springframework.data.domain.ExampleSpec.MatcherConfigurer; +import org.springframework.data.domain.ExampleSpec.NoOpPropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.StringMatcher; +import org.springframework.data.domain.TypedExampleSpec; + +/** + * Test for {@link ExampleSpecAccessor}. + * + * @author Mark Paluch + * @soundtrack Cabballero - Dancing With Tears In My Eyes (Dance Maxi) + */ +public class ExampleSpecAccessorTypedExampleSpecUnitTests { + + private Person person; + private TypedExampleSpec exampleSpec; + private ExampleSpecAccessor exampleSpecAccessor; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + exampleSpec = ExampleSpec.typed(Person.class); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void isTypedShouldReturnTrue() { + assertThat(exampleSpecAccessor.isTyped(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() { + assertThat(exampleSpecAccessor.getDefaultStringMatcher(), equalTo(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() { + + exampleSpec = ExampleSpec.typed(Person.class).withIncludeNullValues(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getNullHandler(), equalTo(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldIgnorePaths() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoredPath("firstname"), equalTo(true)); + assertThat(exampleSpecAccessor.isIgnoredPath("lastname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatDoesNotHavePropertySpecifier() { + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), + equalTo(exampleSpec.getDefaultStringMatcher())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHavePropertySpecifier() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.CONTAINING); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains().caseSensitive()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherStarting() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.startsWith()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherContaining() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherRegex() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.regex()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.REGEX)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldFavorStringMatcherDefinedForPathOverConfiguredDefaultStringMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.ENDING) + .withMatcher("firstname", new GenericPropertyMatcher().contains()) + .withMatcher("address.city", new GenericPropertyMatcher().startsWith()) + .withMatcher("lastname", new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getPropertySpecifiers(), hasSize(3)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("lastname"), equalTo(StringMatcher.ENDING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("unknownProperty"), equalTo(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatHasPropertySpecifierWithoutStringMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("unknownProperty"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() { + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseEnabled(), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldFavorPathSpecificSettings() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsNoOpValueTransformerByDefault() { + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), + instanceOf(NoOpPropertyValueTransformer.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsConfigurtedTransformerForPath() { + + PropertyValueTransformer transformer = new PropertyValueTransformer() { + + @Override + public Object convert(Object source) { + return source.toString(); + } + }; + + exampleSpec = ExampleSpec.typed(Person.class).withTransformer("firstname", transformer); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), equalTo(transformer)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsFalseIfNoneDefined() { + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(true)); + } + + static class Person { + + String firstname; + } + +} diff --git a/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java new file mode 100644 index 0000000000..c7536b93a2 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2016 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.repository.core.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatcher; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatchers; +import org.springframework.data.domain.ExampleSpec.MatcherConfigurer; +import org.springframework.data.domain.ExampleSpec.NoOpPropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.StringMatcher; + +/** + * Test for {@link ExampleSpecAccessor}. + * + * @author Mark Paluch + * @soundtrack Alex Wackii and Tolga Uzulmez - Diamond Rush (original mix) + */ +public class ExampleSpecAccessorUntypedExampleSpecUnitTests { + + private Person person; + private ExampleSpec exampleSpec; + private ExampleSpecAccessor exampleSpecAccessor; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + exampleSpec = ExampleSpec.untyped(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() { + assertThat(exampleSpecAccessor.getDefaultStringMatcher(), equalTo(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() { + + exampleSpec = ExampleSpec.untyped().withIncludeNullValues(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getNullHandler(), equalTo(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldIgnorePaths() { + + exampleSpec = ExampleSpec.untyped().withIgnorePaths("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoredPath("firstname"), equalTo(true)); + assertThat(exampleSpecAccessor.isIgnoredPath("lastname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatDoesNotHavePropertySpecifier() { + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), + equalTo(exampleSpec.getDefaultStringMatcher())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHavePropertySpecifier() { + + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.CONTAINING); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains().caseSensitive()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherStarting() { + + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.startsWith()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherContaining() { + + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherRegex() { + + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.regex()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.REGEX)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldFavorStringMatcherDefinedForPathOverConfiguredDefaultStringMatcher() { + + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING) + .withMatcher("firstname", new GenericPropertyMatcher().contains()) + .withMatcher("address.city", new GenericPropertyMatcher().startsWith()) + .withMatcher("lastname", new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getPropertySpecifiers(), hasSize(3)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("lastname"), equalTo(StringMatcher.ENDING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("unknownProperty"), equalTo(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatHasPropertySpecifierWithoutStringMatcher() { + + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("unknownProperty"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() { + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseEnabled(), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldFavorPathSpecificSettings() { + + exampleSpec = ExampleSpec.untyped().withIgnoreCase("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsNoOpValueTransformerByDefault() { + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), + instanceOf(NoOpPropertyValueTransformer.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsConfigurtedTransformerForPath() { + + PropertyValueTransformer transformer = new PropertyValueTransformer() { + + @Override + public Object convert(Object source) { + return source.toString(); + } + }; + + exampleSpec = ExampleSpec.untyped().withTransformer("firstname", transformer); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), equalTo(transformer)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsFalseIfNoneDefined() { + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { + + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(true)); + } + + static class Person { + + String firstname; + } + +}