diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc new file mode 100644 index 000000000..1c07fb3a3 --- /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 index 6b8f6d76d..3fa95c20e 100644 --- a/src/main/java/org/springframework/data/domain/Example.java +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -15,480 +15,126 @@ */ package org.springframework.data.domain; -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.data.domain.Example.NullHandler; -import org.springframework.data.domain.PropertySpecifier.NoOpPropertyValueTransformer; -import org.springframework.data.domain.PropertySpecifier.PropertyValueTransformer; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * Support for query by example (QBE). + * 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 sampleObject; - - private NullHandler nullHandler = NullHandler.IGNORE; - private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; - private PropertySpecifiers propertySpecifiers = new PropertySpecifiers(); - private Set ignoredPaths = new LinkedHashSet(); - - private boolean ignoreCase = false; + private final T probe; + private final ExampleSpec exampleSpec; /** * Create a new {@link Example} including all non-null properties by default. - * - * @param sampleObject The example to use. Must not be {@literal null}. + * + * @param probe The probe to use. Must not be {@literal null}. */ - public Example(T sampleObject) { + @SuppressWarnings("unchecked") + private Example(T probe) { - Assert.notNull(sampleObject, "SampleObject must not be null!"); - this.sampleObject = sampleObject; + 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 getSampleObject() { - return sampleObject; + public T getProbe() { + return probe; } /** - * Get defined null handling. - * - * @return never {@literal null} - */ - public NullHandler getNullHandler() { - return nullHandler; - } - - /** - * Get defined {@link StringMatcher}. - * + * Get the {@link ExampleSpec} used. + * * @return never {@literal null}. */ - public StringMatcher getDefaultStringMatcher() { - return defaultStringMatcher; + public ExampleSpec getExampleSpec() { + return exampleSpec; } /** - * @return {@literal true} if {@link String} should be matched with ignore case option. - */ - public boolean isIngnoreCaseEnabled() { - return this.ignoreCase; - } - - /** - * @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 Collections.unmodifiableSet(ignoredPaths); - } - - /** - * @return unmodifiable {@link Collection} of {@link PropertySpecifier}s. - */ - public Collection getPropertySpecifiers() { - return propertySpecifiers.getSpecifiers(); - } - - /** - * @param path Dot-Path to property. - * @return {@literal true} in case {@link PropertySpecifier} defined for given path. - */ - public boolean hasPropertySpecifier(String path) { - return propertySpecifiers.hasSpecifierForPath(path); - } - - /** - * Get the {@link 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 PropertySpecifier} defined for path. - */ - public PropertySpecifier getPropertySpecifier(String path) { - return propertySpecifiers.getForPath(path); - } - - /** - * @return true if at least one {@link PropertySpecifier} defined. - */ - public boolean hasPropertySpecifiers() { - return this.propertySpecifiers.hasValues(); - } - - /** - * Get the {@link StringMatcher} for a given path or return the default one if none defined. - * - * @param path - * @return never {@literal null}. - */ - public StringMatcher getStringMatcherForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return getDefaultStringMatcher(); - } - - PropertySpecifier specifier = getPropertySpecifier(path); - return specifier.getStringMatcher() != null ? specifier.getStringMatcher() : getDefaultStringMatcher(); - } - - /** - * 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 isIngnoreCaseEnabled(); - } - - PropertySpecifier specifier = getPropertySpecifier(path); - return specifier.getIgnoreCase() != null ? specifier.getIgnoreCase().booleanValue() : isIngnoreCaseEnabled(); - } - - /** - * Get the ignore case flag for a given path or return {@link NoOpPropertyValueTransformer} if none defined. - * - * @param path - * @return never {@literal null}. - */ - public PropertyValueTransformer getValueTransformerForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return NoOpPropertyValueTransformer.INSTANCE; - } - - return getPropertySpecifier(path).getPropertyValueTransformer(); - } - - /** - * Get the actual type for the example used. This is usually the given class, but the original class in case of a + * 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 getSampleType() { - return (Class) ClassUtils.getUserClass(sampleObject.getClass()); + public Class getProbeType() { + return (Class) ClassUtils.getUserClass(probe.getClass()); } /** - * Create a new {@link Example} including all non-null properties by default. - * - * @param sampleObject must not be {@literal null}. + * Get the actual result type for the query. The result type can be different when using {@link TypedExampleSpec}. + * * @return + * @see ClassUtils#getUserClass(Class) */ - public static Example exampleOf(T sampleObject) { - return new Example(sampleObject); - } - - /** - * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. - * - * @param sampleObject must not be {@literal null}. - * @return - */ - public static Example exampleOf(T value, String... ignoredProperties) { - return new Builder(value).ignore(ignoredProperties).get(); - } - - /** - * Create new {@link Builder} for specifying {@link Example}. - * - * @param sampleObject must not be {@literal null}. - * @return - * @see Builder - */ - public static Builder newExampleOf(T sampleObject) { - return new Builder(sampleObject); - } - - /** - * Builder for specifying desired behavior of {@link Example}. - * - * @author Christoph Strobl - * @param - */ - public static class Builder { - - private Example example; - - Builder(T sampleObject) { - example = new Example(sampleObject); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling - * @return - * @see Builder#nullHandling(NullHandler) - */ - public Builder withNullHandler(NullHandler nullHandling) { - return handleNullValues(nullHandling); - } - - /** - * Sets default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher - * @return - * @see Builder#matchStrings(StringMatcher) - */ - public Builder withStringMatcher(StringMatcher stringMatcher) { - return matchStrings(stringMatcher); - } - - /** - * Adds {@link PropertySpecifier} used for {@link Example}. - * - * @param specifier - * @return - * @see Builder#specify(PropertySpecifier...) - */ - public Builder withPropertySpecifier(PropertySpecifier... specifiers) { - return specify(specifiers); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling Defaulted to {@link NullHandler#INCLUDE} in case of {@literal null}. - * @return - */ - public Builder handleNullValues(NullHandler nullHandling) { - - example.nullHandler = nullHandling == null ? NullHandler.IGNORE : nullHandling; - return this; - } - - /** - * Sets treatment of {@literal null} values to {@link NullHandler#INCLUDE} - * - * @return - */ - public Builder includeNullValues() { - return handleNullValues(NullHandler.INCLUDE); - } - - /** - * Sets treatment of {@literal null} values to {@link NullHandler#IGNORE} - * - * @return - */ - public Builder ignoreNullValues() { - return handleNullValues(NullHandler.IGNORE); - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @return - */ - public Builder matchStrings(StringMatcher stringMatcher) { - return matchStrings(stringMatcher, example.ignoreCase); - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @param ignoreCase - * @return - */ - public Builder matchStrings(StringMatcher stringMatching, boolean ignoreCase) { - - example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; - example.ignoreCase = ignoreCase; - return this; - } - - /** - * Enable case ignoring string matching. - * - * @return - */ - public Builder matchStringsWithIgnoreCase() { - example.ignoreCase = true; - return this; - } - - /** - * Set string matching to {@link StringMatcher#STARTING} - * - * @return - */ - public Builder matchStringsStartingWith() { - return matchStrings(StringMatcher.STARTING); - } - - /** - * Set string matching to {@link StringMatcher#ENDING} - * - * @return - */ - public Builder matchStringsEndingWith() { - return matchStrings(StringMatcher.ENDING); - } - - /** - * Set string matching to {@link StringMatcher#CONTAINING} - * - * @return - */ - public Builder matchStringsContaining() { - return matchStrings(StringMatcher.CONTAINING); - } - - /** - * Define specific property handling. - * - * @param specifiers - * @return - */ - public Builder specify(PropertySpecifier... specifiers) { - - for (PropertySpecifier specifier : specifiers) { - example.propertySpecifiers.add(specifier); - } - return this; - } - - /** - * Ignore given properties. - * - * @param ignoredProperties - * @return - */ - public Builder ignore(String... ignoredProperties) { - - for (String ignoredProperty : ignoredProperties) { - example.ignoredPaths.add(ignoredProperty); - } - return this; - } - - /** - * @return {@link Example} as defined. - */ - public Example get() { - return this.example; - } - } - - /** - * Null handling for creating criterion out of an {@link Example}. - * - * @author Christoph Strobl - */ - public static enum NullHandler { - - INCLUDE, IGNORE - } - - /** - * Match modes for treatment of {@link String} values. - * - * @author Christoph Strobl - */ - public static enum StringMatcher { - - /** - * Store specific default. - */ - DEFAULT(null), - /** - * Matches the exact string - */ - EXACT(Type.SIMPLE_PROPERTY), - /** - * Matches string starting with pattern - */ - STARTING(Type.STARTING_WITH), - /** - * Matches string ending with pattern - */ - ENDING(Type.ENDING_WITH), - /** - * Matches string containing pattern - */ - CONTAINING(Type.CONTAINING), - /** - * Treats strings as regular expression patterns - */ - REGEX(Type.REGEX); - - private Type type; - - private StringMatcher(Type type) { - this.type = type; - } - - /** - * Get the according {@link Part.Type}. - * - * @return {@literal null} for {@link StringMatcher#DEFAULT}. - */ - public Type getPartType() { - return type; - } - - } - - private static class PropertySpecifiers { - - private Map propertySpecifiers = new LinkedHashMap(); - - 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(); - } + @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 000000000..9d8051fab --- /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/PropertySpecifier.java b/src/main/java/org/springframework/data/domain/PropertySpecifier.java deleted file mode 100644 index 8ff51c54b..000000000 --- a/src/main/java/org/springframework/data/domain/PropertySpecifier.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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.core.convert.converter.Converter; -import org.springframework.data.domain.Example.StringMatcher; -import org.springframework.data.domain.PropertySpecifier.PropertyValueTransformer; -import org.springframework.util.Assert; - -/** - * Define specific property handling for a Dot-Path. - * - * @author Christoph Strobl - * @since 1.12 - */ -public class PropertySpecifier { - - private final String path; - - private StringMatcher stringMatcher; - private Boolean ignoreCase; - - private 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; - } - - /** - * 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 true} in case {@link StringMatcher} defined. - */ - public boolean hasStringMatcher() { - return this.stringMatcher != null; - } - - /** - * @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); - } - - /** - * Creates new case ignoring {@link PropertySpecifier} for given path. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static PropertySpecifier ignoreCase(String propertyPath) { - return new Builder(propertyPath).matchStringsWithIgnoreCase().get(); - } - - /** - * Creates new {@link PropertySpecifier} using given {@link PropertyValueTransformer}. - * - * @param propertyPath must not be {@literal null}. - * @param valueTransformer should not be {@literal null}, will be defaulted to {@link NoOpPropertyValueTransformer}. - * @return - */ - public static PropertySpecifier transform(String propertyPath, PropertyValueTransformer valueTransformer) { - return new Builder(propertyPath).withValueTransformer( - valueTransformer != null ? valueTransformer : NoOpPropertyValueTransformer.INSTANCE).get(); - } - - /** - * Create new {@link Builder} for specifying {@link PropertySpecifier}. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static Builder newPropertySpecifier(String propertyPath) { - return new Builder(propertyPath); - } - - /** - * Builder for specifying desired behavior of {@link PropertySpecifier}. - * - * @author Christoph Strobl - */ - public static class Builder { - - private PropertySpecifier specifier; - - Builder(String path) { - specifier = new PropertySpecifier(path); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - * @see Builder#stringMatcher(StringMatcher) - */ - public Builder withStringMatcher(StringMatcher stringMatcher) { - return matchString(stringMatcher); - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - * @see Builder#valueTransformer(PropertyValueTransformer) - */ - public Builder withValueTransformer(PropertyValueTransformer valueTransformer) { - - specifier.valueTransformer = valueTransformer; - return this; - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - * @see Builder#valueTransformer(PropertyValueTransformer) - */ - public Builder transforming(PropertyValueTransformer valueTransformer) { - return withValueTransformer(valueTransformer); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - */ - public Builder matchString(StringMatcher stringMatcher) { - return matchString(stringMatcher, specifier.ignoreCase); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @param ignoreCase - * @return - */ - public Builder matchString(StringMatcher stringMatcher, Boolean ignoreCase) { - - specifier.stringMatcher = stringMatcher; - specifier.ignoreCase = ignoreCase; - return this; - } - - /** - * Enable case ignoring string matching. - * - * @return - */ - public Builder matchStringsWithIgnoreCase() { - specifier.ignoreCase = true; - return this; - } - - /** - * Set string matching to {@link StringMatcher#STARTING} - * - * @return - */ - public Builder matchStringStartingWith() { - return matchString(StringMatcher.STARTING); - } - - /** - * Set string matching to {@link StringMatcher#ENDING} - * - * @return - */ - public Builder matchStringEndingWith() { - return matchString(StringMatcher.ENDING); - } - - /** - * Set string matching to {@link StringMatcher#CONTAINING} - * - * @return - */ - public Builder matchStringContaining() { - return matchString(StringMatcher.CONTAINING); - } - - /** - * @return {@link PropertySpecifier} as defined. - */ - public PropertySpecifier get() { - return this.specifier; - } - } - - public static interface PropertyValueTransformer extends Converter { - // TODO: should we use the converter interface directly or not at all? - } - - /** - * @author Christoph Strobl - * @since 1.12 - */ - static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { - - INSTANCE; - - @Override - public Object convert(Object source) { - return source; - } - - } -} 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 000000000..1fb17b19a --- /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 000000000..3afc2b90f --- /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 000000000..a5421e27f --- /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 000000000..fbd2fcca3 --- /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 index 223864dfa..8cff9970e 100644 --- a/src/test/java/org/springframework/data/domain/ExampleUnitTests.java +++ b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java @@ -15,24 +15,18 @@ */ package org.springframework.data.domain; -import static org.hamcrest.collection.IsEmptyCollection.*; -import static org.hamcrest.core.Is.*; -import static org.hamcrest.core.IsCollectionContaining.*; import static org.hamcrest.core.IsEqual.*; -import static org.hamcrest.core.IsInstanceOf.*; import static org.junit.Assert.*; import static org.springframework.data.domain.Example.*; -import static org.springframework.data.domain.PropertySpecifier.*; import org.junit.Before; import org.junit.Test; -import org.springframework.data.domain.Example.NullHandler; -import org.springframework.data.domain.Example.StringMatcher; -import org.springframework.data.domain.PropertySpecifier.NoOpPropertyValueTransformer; -import org.springframework.data.domain.PropertySpecifier.PropertyValueTransformer; /** + * Test for {@link Example}. + * * @author Christoph Strobl + * @author Mark Paluch */ public class ExampleUnitTests { @@ -45,7 +39,7 @@ public class ExampleUnitTests { person = new Person(); person.firstname = "rand"; - example = exampleOf(person); + example = of(person); } /** @@ -53,196 +47,7 @@ public class ExampleUnitTests { */ @Test(expected = IllegalArgumentException.class) public void exampleOfNullThrowsException() { - new Example(null); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldUseDefaultStringMatcher() { - assertThat(example.getDefaultStringMatcher(), equalTo(StringMatcher.DEFAULT)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldUseDefaultStringMatcherForPathThatDoesNotHavePropertySpecifier() { - assertThat(example.getStringMatcherForPath("firstname"), equalTo(example.getDefaultStringMatcher())); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHavePropertySpecifier() { - - example = newExampleOf(person).withStringMatcher(StringMatcher.CONTAINING).get(); - - assertThat(example.getDefaultStringMatcher(), equalTo(StringMatcher.CONTAINING)); - assertThat(example.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcher() { - - example = newExampleOf(person).withPropertySpecifier( - PropertySpecifier.newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()).get(); - - assertThat(example.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldFavorStringMatcherDefinedForPathOverConfiguredDefaultStringMatcher() { - - example = newExampleOf(person) - .withStringMatcher(StringMatcher.STARTING) - .withPropertySpecifier( - PropertySpecifier.newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()).get(); - - assertThat(example.getDefaultStringMatcher(), equalTo(StringMatcher.STARTING)); - assertThat(example.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void exampleShouldUseDefaultStringMatcherForPathThatHasPropertySpecifierWithoutStringMatcher() { - - example = newExampleOf(person).withStringMatcher(StringMatcher.STARTING) - .withPropertySpecifier(ignoreCase("firstname")).get(); - - assertThat(example.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void isIgnoredPathShouldReturnFalseWhenNoPathsIgnored() { - - assertThat(example.getIgnoredPaths(), is(empty())); - assertThat(example.isIgnoredPath("firstname"), is(false)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void isIgnoredPathShouldReturnTrueWhenNoPathsIgnored() { - - example = newExampleOf(person).ignore("firstname").get(); - - assertThat(example.getIgnoredPaths(), hasItem("firstname")); - assertThat(example.isIgnoredPath("firstname"), is(true)); - } - - /** - * @see DATACMNS-810 - */ - @Test(expected = UnsupportedOperationException.class) - public void ignoredPathsShouldNotAllowModification() { - example.getIgnoredPaths().add("o_O"); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void ignoreCaseShouldReturnFalseByDefault() { - - assertThat(example.isIngnoreCaseEnabled(), is(false)); - assertThat(example.isIgnoreCaseForPath("firstname"), is(false)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { - - example = newExampleOf(person).matchStringsWithIgnoreCase().get(); - - assertThat(example.isIngnoreCaseEnabled(), is(true)); - assertThat(example.isIgnoreCaseForPath("firstname"), is(true)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void ignoreCaseShouldFavorPathSpecificSettings() { - - example = newExampleOf(person).withPropertySpecifier(ignoreCase("firstname")).get(); - - assertThat(example.isIngnoreCaseEnabled(), is(false)); - assertThat(example.isIgnoreCaseForPath("firstname"), is(true)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void getValueTransformerForPathReturnsNoOpValueTransformerByDefault() { - assertThat(example.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(); - } - }; - - example = newExampleOf(person).withPropertySpecifier( - newPropertySpecifier("firstname").withValueTransformer(transformer).get()).get(); - - assertThat(example.getValueTransformerForPath("firstname"), equalTo(transformer)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void hasPropertySpecifiersReturnsFalseIfNoneDefined() { - assertThat(example.hasPropertySpecifiers(), is(false)); - } - - /** - * @see DATACMNS-810 - */ - @Test(expected = UnsupportedOperationException.class) - public void getPropertiesSpecifiersShouldNotAllowAddingSpecifiers() { - example.getPropertySpecifiers().add(ignoreCase("firstname")); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { - - example = newExampleOf(person) - .withStringMatcher(StringMatcher.STARTING) - .withPropertySpecifier( - PropertySpecifier.newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()).get(); - - assertThat(example.hasPropertySpecifiers(), is(true)); + Example.of(null); } /** @@ -250,25 +55,38 @@ public class ExampleUnitTests { */ @Test public void getSampleTypeRetunsSampleObjectsClass() { - assertThat(example.getSampleType(), equalTo(Person.class)); + assertThat(example.getProbeType(), equalTo(Person.class)); } /** * @see DATACMNS-810 */ @Test - public void getNullHandlerShouldReturnIgnoreByDefault() { - assertThat(example.getNullHandler(), is(NullHandler.IGNORE)); + 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 getNullHandlerShouldReturnConfiguredHandler() { + public void createUntypedExample() { - example = newExampleOf(person).handleNullValues(NullHandler.INCLUDE).get(); - assertThat(example.getNullHandler(), is(NullHandler.INCLUDE)); + 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 { 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 000000000..aea31209a --- /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 000000000..195d6fb22 --- /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 000000000..c7536b93a --- /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; + } + +}