From 5a613430b7845019f9c097d431ce83fcc8ccfe15 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 27 Jan 2016 11:38:52 +0100 Subject: [PATCH 1/6] DATACMNS-810 - Add core types for Query By Example support. Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0710461991..32be87f991 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 1.12.0.BUILD-SNAPSHOT + 1.12.0.DATACMNS-810-SNAPSHOT Spring Data Core From 3871379cc359c6e06c692e550c27f320d3e8212c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 27 Jan 2016 13:55:58 +0100 Subject: [PATCH 2/6] DATACMNS-810 - Add core types for Query By Example support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added required types for Query By Example which should be used by the individual store implementations. Generally the Example type captures a sample object and allow various settings concerning the mapping into an actual query. So there’s configuration options for handling null values, string matching, property paths to ignore,... ---- Required to build: DATAJPA-218, DATAMONGO-1245 --- .../springframework/data/domain/Example.java | 494 ++++++++++++++++++ .../data/domain/PropertySpecifier.java | 267 ++++++++++ .../data/domain/ExampleUnitTests.java | 279 ++++++++++ 3 files changed, 1040 insertions(+) create mode 100644 src/main/java/org/springframework/data/domain/Example.java create mode 100644 src/main/java/org/springframework/data/domain/PropertySpecifier.java create mode 100644 src/test/java/org/springframework/data/domain/ExampleUnitTests.java diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java new file mode 100644 index 0000000000..6b8f6d76d8 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -0,0 +1,494 @@ +/* + * 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.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). + * + * @author Christoph Strobl + * @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; + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param sampleObject The example to use. Must not be {@literal null}. + */ + public Example(T sampleObject) { + + Assert.notNull(sampleObject, "SampleObject must not be null!"); + this.sampleObject = sampleObject; + } + + /** + * Get the example used. + * + * @return never {@literal null}. + */ + public T getSampleObject() { + return sampleObject; + } + + /** + * Get defined null handling. + * + * @return never {@literal null} + */ + public NullHandler getNullHandler() { + return nullHandler; + } + + /** + * Get defined {@link StringMatcher}. + * + * @return never {@literal null}. + */ + public StringMatcher getDefaultStringMatcher() { + return defaultStringMatcher; + } + + /** + * @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 + * CGLIB-generated subclass. + * + * @return + * @see ClassUtils#getUserClass(Class) + */ + @SuppressWarnings("unchecked") + public Class getSampleType() { + return (Class) ClassUtils.getUserClass(sampleObject.getClass()); + } + + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param sampleObject must not be {@literal null}. + * @return + */ + 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(); + } + } + +} diff --git a/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/src/main/java/org/springframework/data/domain/PropertySpecifier.java new file mode 100644 index 0000000000..8ff51c54b6 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/PropertySpecifier.java @@ -0,0 +1,267 @@ +/* + * 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/test/java/org/springframework/data/domain/ExampleUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java new file mode 100644 index 0000000000..223864dfa3 --- /dev/null +++ b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java @@ -0,0 +1,279 @@ +/* + * 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.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; + +/** + * @author Christoph Strobl + */ +public class ExampleUnitTests { + + private Person person; + private Example example; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + example = exampleOf(person); + } + + /** + * @see DATACMNS-810 + */ + @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)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getSampleTypeRetunsSampleObjectsClass() { + assertThat(example.getSampleType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getNullHandlerShouldReturnIgnoreByDefault() { + assertThat(example.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getNullHandlerShouldReturnConfiguredHandler() { + + example = newExampleOf(person).handleNullValues(NullHandler.INCLUDE).get(); + assertThat(example.getNullHandler(), is(NullHandler.INCLUDE)); + } + + static class Person { + + String firstname; + } + +} From c9d9f9097cf2f94743b0a5cb5a39a85dcb9e908b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 25 Feb 2016 09:31:24 +0100 Subject: [PATCH 3/6] DATACMNS-810 - Refactor Query by Example API. Split Example and ExampleSpec to create reusable components. Refactor builder pattern to a fluent API that creates immutable instances. Split user and framework API, Example and ExampleSpec are user API, created ExampleSpecAccessor for modules to access example spec configuration. Create static methods in GenericPropertyMatchers to ease creation of matchers in a readable style. Convert PropertySpecifier to inner class and move PropertySpecifiers to ExampleSpec. --- .../springframework/data/domain/Example.java | 461 +--------- .../data/domain/ExampleSpec.java | 797 ++++++++++++++++++ .../data/domain/ExampleSpecAccessor.java | 149 ++++ .../data/domain/PropertySpecifier.java | 267 ------ .../query/QueryByExampleExecutor.java | 85 ++ .../domain/ExampleSpecAccessorUnitTests.java | 305 +++++++ .../data/domain/ExampleSpecUnitTests.java | 220 +++++ .../data/domain/ExampleUnitTests.java | 223 +---- 8 files changed, 1601 insertions(+), 906 deletions(-) create mode 100644 src/main/java/org/springframework/data/domain/ExampleSpec.java create mode 100644 src/main/java/org/springframework/data/domain/ExampleSpecAccessor.java delete mode 100644 src/main/java/org/springframework/data/domain/PropertySpecifier.java create mode 100644 src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java create mode 100644 src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java create mode 100644 src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java index 6b8f6d76d8..2f9eddfb7f 100644 --- a/src/main/java/org/springframework/data/domain/Example.java +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -15,18 +15,6 @@ */ 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; @@ -34,461 +22,92 @@ * Support for query by example (QBE). * * @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}. - */ - public Example(T sampleObject) { - - Assert.notNull(sampleObject, "SampleObject must not be null!"); - this.sampleObject = sampleObject; - } - - /** - * Get the example used. - * - * @return never {@literal null}. - */ - public T getSampleObject() { - return sampleObject; - } - - /** - * Get defined null handling. - * - * @return never {@literal null} - */ - public NullHandler getNullHandler() { - return nullHandler; - } - - /** - * Get defined {@link StringMatcher}. - * - * @return never {@literal null}. - */ - public StringMatcher getDefaultStringMatcher() { - return defaultStringMatcher; - } - - /** - * @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. + * + * @param probe The probe to use. Must not be {@literal null}. */ - 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); - } + @SuppressWarnings("unchecked") + public Example(T probe) { - /** - * 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); - } + Assert.notNull(probe, "Probe must not be null!"); - /** - * @return true if at least one {@link PropertySpecifier} defined. - */ - public boolean hasPropertySpecifiers() { - return this.propertySpecifiers.hasValues(); + this.probe = probe; + this.exampleSpec = ExampleSpec.of((Class) probe.getClass()); } /** - * Get the {@link StringMatcher} for a given path or return the default one if none defined. - * - * @param path - * @return never {@literal null}. + * 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}. */ - public StringMatcher getStringMatcherForPath(String path) { + public Example(T probe, ExampleSpec exampleSpec) { - if (!hasPropertySpecifier(path)) { - return getDefaultStringMatcher(); - } + Assert.notNull(probe, "Probe must not be null!"); - PropertySpecifier specifier = getPropertySpecifier(path); - return specifier.getStringMatcher() != null ? specifier.getStringMatcher() : getDefaultStringMatcher(); + this.probe = probe; + this.exampleSpec = exampleSpec; } /** - * Get the ignore case flag for a given path or return the default one if none defined. - * - * @param path + * Get the example used. + * * @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(); + public T getProbe() { + return probe; } /** - * Get the ignore case flag for a given path or return {@link NoOpPropertyValueTransformer} if none defined. - * - * @param path + * Get the {@link ExampleSpec} used. + * * @return never {@literal null}. */ - public PropertyValueTransformer getValueTransformerForPath(String path) { - - if (!hasPropertySpecifier(path)) { - return NoOpPropertyValueTransformer.INSTANCE; - } - - return getPropertySpecifier(path).getPropertyValueTransformer(); + public ExampleSpec getExampleSpec() { + return exampleSpec; } /** - * 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}. + * + * @param probe must not be {@literal null}. * @return */ - public static Example exampleOf(T sampleObject) { - return new Example(sampleObject); + public static Example of(T probe) { + return new Example(probe); } /** - * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. - * - * @param sampleObject must not be {@literal null}. + * Create a new {@link Example} with a configured {@link ExampleSpec}. + * + * @param probe must not be {@literal null}. + * @param exampleSpec 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(); - } + public static Example of(T probe, ExampleSpec exampleSpec) { + return new Example(probe, exampleSpec); } } diff --git a/src/main/java/org/springframework/data/domain/ExampleSpec.java b/src/main/java/org/springframework/data/domain/ExampleSpec.java new file mode 100644 index 0000000000..3430563ab8 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ExampleSpec.java @@ -0,0 +1,797 @@ +/* + * 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.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.Type; +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} but they can be refined using the various + * {@code with...} methods in a fluent style. A {@code with...} method creates a new instance of {@link ExampleSpec} + * containing all settings from the current instance but sets the value in the new instance. Null-handling defaults to + * {@link NullHandler#IGNORE} and case-sensitive {@link StringMatcher#DEFAULT} string matching. + * + * @author Christoph Strobl + * @author Mark Paluch + * @param + * @since 1.12 + */ +public class ExampleSpec { + + private final Class type; + private final NullHandler nullHandler; + private final StringMatcher defaultStringMatcher; + private final boolean defaultIgnoreCase; + private final PropertySpecifiers propertySpecifiers; + private final Set ignoredPaths; + + private ExampleSpec(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + this.nullHandler = NullHandler.IGNORE; + this.defaultStringMatcher = StringMatcher.DEFAULT; + this.propertySpecifiers = new PropertySpecifiers(); + this.defaultIgnoreCase = false; + this.ignoredPaths = Collections.emptySet(); + } + + private ExampleSpec(Class type, NullHandler nullHandler, StringMatcher defaultStringMatcher, + PropertySpecifiers propertySpecifiers, Set ignoredPaths, boolean defaultIgnoreCase) { + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + this.nullHandler = nullHandler; + this.defaultStringMatcher = defaultStringMatcher; + this.propertySpecifiers = propertySpecifiers; + this.ignoredPaths = Collections.unmodifiableSet(ignoredPaths); + this.defaultIgnoreCase = defaultIgnoreCase; + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore the {@code propertyPaths}. + * + * @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(type, nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#STARTING}. + * + * @return + */ + public ExampleSpec withStringMatcherStarting() { + return new ExampleSpec(type, nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#ENDING}. + * + * @return + */ + public ExampleSpec withStringMatcherEnding() { + return new ExampleSpec(type, nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#CONTAINING}. + * + * @return + */ + public ExampleSpec withStringMatcherContaining() { + return new ExampleSpec(type, nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set the {@code defaultStringMatcher}. + * + * @param defaultStringMatcher must not be {@literal null}. + * @return + */ + public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { + + Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); + + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with ignoring case sensitivity by + * default. + * + * @return + */ + public ExampleSpec withIgnoreCase() { + return withIgnoreCase(true); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with {@code defaultIgnoreCase}. + * + * @param defaultIgnoreCase + * @return + */ + public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * + * @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); + + propertySpecifier.stringMatcher = genericPropertyMatcher.stringMatcher; + propertySpecifier.ignoreCase = genericPropertyMatcher.ignoreCase; + propertySpecifier.valueTransformer = genericPropertyMatcher.valueTransformer; + + propertySpecifiers.add(propertySpecifier); + + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@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 = createOrClonePropertySpecifier(propertyPath, propertySpecifiers); + + propertySpecifier.valueTransformer = propertyValueTransformer; + + propertySpecifiers.add(propertySpecifier); + + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore case sensitivity for the + * {@code propertyPaths}. + * + * @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 = createOrClonePropertySpecifier(propertyPath, propertySpecifiers); + propertySpecifier.ignoreCase = true; + propertySpecifiers.add(propertySpecifier); + } + + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + private PropertySpecifier createOrClonePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { + PropertySpecifier propertySpecifier; + + if (propertySpecifiers.hasSpecifierForPath(propertyPath)) { + propertySpecifier = new PropertySpecifier(propertyPath); + PropertySpecifier existing = propertySpecifiers.getForPath(propertyPath); + propertySpecifier.ignoreCase = existing.ignoreCase; + propertySpecifier.stringMatcher = existing.stringMatcher; + } else { + propertySpecifier = new PropertySpecifier(propertyPath); + } + return propertySpecifier; + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@link NullHandler#INCLUDE}. + * + * @return + */ + public ExampleSpec withIncludeNullValues() { + return new ExampleSpec(type, NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@link NullHandler#IGNORE}. + * + * @return + */ + public ExampleSpec withIgnoreNullValues() { + return new ExampleSpec(type, NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@code nullHandler}. + * + * @param nullHandler must not be {@literal null}. + * @return + */ + public ExampleSpec withNullHandler(NullHandler nullHandler) { + + Assert.notNull(nullHandler, "NullHandler must not be null!"); + return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link Example} containing the {@code probe}. + * + * @param probe must not be {@literal null}. + * @return + */ + public Example createExample(T probe) { + return Example.of(probe, this); + } + + /** + * 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; + } + + PropertySpecifiers getPropertySpecifiers() { + return propertySpecifiers; + } + + /** + * Create a new {@link ExampleSpec} including all non-null properties by default. + * + * @param type must not be {@literal null}. + * @return + */ + public static ExampleSpec of(Class type) { + return new ExampleSpec(type); + } + + /** + * 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 { + + private StringMatcher stringMatcher = null; + private Boolean ignoreCase = null; + private PropertyValueTransformer valueTransformer = NoOpPropertyValueTransformer.INSTANCE; + + /** + * 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; + } + + /** + * 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); + } + + } + + /** + * 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(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; + } + + } + + /** + * 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 + */ + 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 + */ + static 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 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); + } + + } + + static class PropertySpecifiers { + + private final Map propertySpecifiers = new LinkedHashMap(); + + public 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/ExampleSpecAccessor.java b/src/main/java/org/springframework/data/domain/ExampleSpecAccessor.java new file mode 100644 index 0000000000..83cdfc0e1b --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ExampleSpecAccessor.java @@ -0,0 +1,149 @@ +/* + * 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.Collection; + +/** + * 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(); + } + +} 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 8ff51c54b6..0000000000 --- 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/repository/query/QueryByExampleExecutor.java b/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java new file mode 100644 index 0000000000..96f5279833 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java @@ -0,0 +1,85 @@ +/* + * 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. + * + * @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. + */ + T 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/ExampleSpecAccessorUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java new file mode 100644 index 0000000000..a36894c456 --- /dev/null +++ b/src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java @@ -0,0 +1,305 @@ +/* + * 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.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +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 Cabballero - Dancing With Tears In My Eyes (Dance Maxi) + */ +public class ExampleSpecAccessorUnitTests { + + private Person person; + private ExampleSpec exampleSpec; + private ExampleSpecAccessor exampleSpecAccessor; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + exampleSpec = ExampleSpec.of(Person.class); + 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.of(Person.class).withIncludeNullValues(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getNullHandler(), equalTo(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldIgnorePaths() { + + exampleSpec = ExampleSpec.of(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.of(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.of(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.of(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.of(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.of(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.of(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.of(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.of(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.of(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.of(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 = ExampleSpec.of(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 = ExampleSpec.of(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/domain/ExampleSpecUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java new file mode 100644 index 0000000000..bd5df6ead2 --- /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.of(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.of(Person.class).withStringMatcherContaining(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.of(Person.class).withStringMatcherStarting(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnEndingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.of(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.of(null); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase(); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase(true); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIncludeNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnore() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnConfiguredValue() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withNullHandler(NullHandler.INCLUDE); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnCorrectProperties() throws Exception { + + ExampleSpec exampleSpec = ExampleSpec.of(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 = ExampleSpec.of(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 = ExampleSpec.of(Person.class).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 223864dfa3..aeaa352ae1 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 void setUp() { person = new Person(); person.firstname = "rand"; - example = exampleOf(person); + example = of(person); } /** @@ -56,219 +50,12 @@ 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)); - } - /** * @see DATACMNS-810 */ @Test public void getSampleTypeRetunsSampleObjectsClass() { - assertThat(example.getSampleType(), equalTo(Person.class)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void getNullHandlerShouldReturnIgnoreByDefault() { - assertThat(example.getNullHandler(), is(NullHandler.IGNORE)); - } - - /** - * @see DATACMNS-810 - */ - @Test - public void getNullHandlerShouldReturnConfiguredHandler() { - - example = newExampleOf(person).handleNullValues(NullHandler.INCLUDE).get(); - assertThat(example.getNullHandler(), is(NullHandler.INCLUDE)); + assertThat(example.getProbeType(), equalTo(Person.class)); } static class Person { From f6007ec034df63c104052cf4591462550e0929a3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Feb 2016 14:54:34 +0100 Subject: [PATCH 4/6] DATACMNS-810 - Add common documentation for Query by Example. --- src/main/asciidoc/query-by-example.adoc | 173 ++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/main/asciidoc/query-by-example.adoc diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc new file mode 100644 index 0000000000..846e187a11 --- /dev/null +++ b/src/main/asciidoc/query-by-example.adoc @@ -0,0 +1,173 @@ +[[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. +* `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 <>. Once the `Example` is constructed it 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 { + + T 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`. + +.Example Spec with customized matching +==== +[source,java] +---- +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +ExampleSpec exampleSpec = ExampleSpec.of(Person.class) <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 `ExampleSpec` for the `Person` type. 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 once it is created. Calls to `with…(…)` create each time a new `ExampleSpec` that carry all settings from the originating instance and setting the specific configuration of the `with…(…)` so 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 example = ExampleSpec.of(Person.class) + .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 example = ExampleSpec.of(Person.class) + .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 + +|=== From 3a6011e7eb2d4cf8b3cacbd24eb2f66006cb8043 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Feb 2016 17:19:34 +0100 Subject: [PATCH 5/6] DATACMNS-810 - Extend Query by Example API to typed and untyped specs. --- .../springframework/data/domain/Example.java | 63 ++-- .../data/domain/ExampleSpec.java | 327 ++++++++++-------- .../data/domain/TypedExampleSpec.java | 310 +++++++++++++++++ .../core/support}/ExampleSpecAccessor.java | 16 +- .../query/QueryByExampleExecutor.java | 17 +- .../data/domain/ExampleSpecUnitTests.java | 30 +- .../data/domain/ExampleUnitTests.java | 33 +- .../domain/TypedExampleSpecUnitTests.java | 231 +++++++++++++ ...SpecAccessorTypedExampleSpecUnitTests.java | 315 +++++++++++++++++ ...cAccessorUntypedExampleSpecUnitTests.java} | 41 +-- 10 files changed, 1171 insertions(+), 212 deletions(-) create mode 100644 src/main/java/org/springframework/data/domain/TypedExampleSpec.java rename src/main/java/org/springframework/data/{domain => repository/core/support}/ExampleSpecAccessor.java (91%) create mode 100644 src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java create mode 100644 src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java rename src/test/java/org/springframework/data/{domain/ExampleSpecAccessorUnitTests.java => repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java} (83%) diff --git a/src/main/java/org/springframework/data/domain/Example.java b/src/main/java/org/springframework/data/domain/Example.java index 2f9eddfb7f..4e7d14b410 100644 --- a/src/main/java/org/springframework/data/domain/Example.java +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -29,7 +29,7 @@ public class Example { private final T probe; - private final ExampleSpec exampleSpec; + private final ExampleSpec exampleSpec; /** * Create a new {@link Example} including all non-null properties by default. @@ -37,12 +37,12 @@ public class Example { * @param probe The probe to use. Must not be {@literal null}. */ @SuppressWarnings("unchecked") - public Example(T probe) { + private Example(T probe) { Assert.notNull(probe, "Probe must not be null!"); this.probe = probe; - this.exampleSpec = ExampleSpec.of((Class) probe.getClass()); + this.exampleSpec = ExampleSpec.untyped(); } /** @@ -51,7 +51,7 @@ public Example(T probe) { * @param probe The probe to use. Must not be {@literal null}. * @param exampleSpec The example specification to use. Must not be {@literal null}. */ - public Example(T probe, ExampleSpec exampleSpec) { + private Example(T probe, ExampleSpec exampleSpec) { Assert.notNull(probe, "Probe must not be null!"); @@ -59,6 +59,38 @@ public Example(T probe, ExampleSpec exampleSpec) { this.exampleSpec = exampleSpec; } + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @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 {@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. * @@ -73,7 +105,7 @@ public T getProbe() { * * @return never {@literal null}. */ - public ExampleSpec getExampleSpec() { + public ExampleSpec getExampleSpec() { return exampleSpec; } @@ -90,24 +122,15 @@ public Class getProbeType() { } /** - * Create a new {@link Example} including all non-null properties by default. - * - * @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 {@link ExampleSpec}. + * Get the actual result type for the query. The result type can be different when using {@link TypedExampleSpec}. * - * @param probe must not be {@literal null}. - * @param exampleSpec must not be {@literal null}. * @return + * @see ClassUtils#getUserClass(Class) */ - public static Example of(T probe, ExampleSpec exampleSpec) { - return new Example(probe, exampleSpec); + @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 index 3430563ab8..9284e08f3d 100644 --- a/src/main/java/org/springframework/data/domain/ExampleSpec.java +++ b/src/main/java/org/springframework/data/domain/ExampleSpec.java @@ -24,8 +24,6 @@ import java.util.Set; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.util.Assert; /** @@ -40,20 +38,16 @@ * @param * @since 1.12 */ -public class ExampleSpec { +public class ExampleSpec { - private final Class type; - private final NullHandler nullHandler; - private final StringMatcher defaultStringMatcher; - private final boolean defaultIgnoreCase; - private final PropertySpecifiers propertySpecifiers; - private final Set ignoredPaths; + protected final NullHandler nullHandler; + protected final StringMatcher defaultStringMatcher; + protected final boolean defaultIgnoreCase; + protected final PropertySpecifiers propertySpecifiers; + protected final Set ignoredPaths; - private ExampleSpec(Class type) { + ExampleSpec() { - Assert.notNull(type, "Type must not be null!"); - - this.type = type; this.nullHandler = NullHandler.IGNORE; this.defaultStringMatcher = StringMatcher.DEFAULT; this.propertySpecifiers = new PropertySpecifiers(); @@ -61,12 +55,9 @@ private ExampleSpec(Class type) { this.ignoredPaths = Collections.emptySet(); } - private ExampleSpec(Class type, NullHandler nullHandler, StringMatcher defaultStringMatcher, - PropertySpecifiers propertySpecifiers, Set ignoredPaths, boolean defaultIgnoreCase) { - - Assert.notNull(type, "Type must not be null!"); + ExampleSpec(NullHandler nullHandler, StringMatcher defaultStringMatcher, PropertySpecifiers propertySpecifiers, + Set ignoredPaths, boolean defaultIgnoreCase) { - this.type = type; this.nullHandler = nullHandler; this.defaultStringMatcher = defaultStringMatcher; this.propertySpecifiers = propertySpecifiers; @@ -74,13 +65,33 @@ private ExampleSpec(Class type, NullHandler nullHandler, StringMatcher defaul this.defaultIgnoreCase = defaultIgnoreCase; } + /** + * Create a new {@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 ExampleSpec} 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); + } + /** * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore the {@code propertyPaths}. * * @param ignoredPaths must not be {@literal null} and not empty. * @return */ - public ExampleSpec withIgnorePaths(String... ignoredPaths) { + public ExampleSpec withIgnorePaths(String... ignoredPaths) { Assert.notEmpty(ignoredPaths, "IgnoredPaths must not be empty!"); Assert.noNullElements(ignoredPaths, "IgnoredPaths must not contain null elements!"); @@ -88,8 +99,7 @@ public ExampleSpec withIgnorePaths(String... ignoredPaths) { Set newIgnoredPaths = new LinkedHashSet(this.ignoredPaths); newIgnoredPaths.addAll(Arrays.asList(ignoredPaths)); - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, - defaultIgnoreCase); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, defaultIgnoreCase); } /** @@ -98,9 +108,8 @@ public ExampleSpec withIgnorePaths(String... ignoredPaths) { * * @return */ - public ExampleSpec withStringMatcherStarting() { - return new ExampleSpec(type, nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + public ExampleSpec withStringMatcherStarting() { + return new ExampleSpec(nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -109,9 +118,8 @@ public ExampleSpec withStringMatcherStarting() { * * @return */ - public ExampleSpec withStringMatcherEnding() { - return new ExampleSpec(type, nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + public ExampleSpec withStringMatcherEnding() { + return new ExampleSpec(nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -120,9 +128,8 @@ public ExampleSpec withStringMatcherEnding() { * * @return */ - public ExampleSpec withStringMatcherContaining() { - return new ExampleSpec(type, nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + public ExampleSpec withStringMatcherContaining() { + return new ExampleSpec(nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -131,12 +138,11 @@ public ExampleSpec withStringMatcherContaining() { * @param defaultStringMatcher must not be {@literal null}. * @return */ - public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { + public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!"); - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -145,7 +151,7 @@ public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { * * @return */ - public ExampleSpec withIgnoreCase() { + public ExampleSpec withIgnoreCase() { return withIgnoreCase(true); } @@ -155,9 +161,8 @@ public ExampleSpec withIgnoreCase() { * @param defaultIgnoreCase * @return */ - public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -168,7 +173,7 @@ public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { * @param matcherConfigurer callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}. * @return */ - public ExampleSpec withMatcher(String propertyPath, MatcherConfigurer matcherConfigurer) { + public ExampleSpec withMatcher(String propertyPath, MatcherConfigurer matcherConfigurer) { Assert.hasText(propertyPath, "PropertyPath must not be empty!"); Assert.notNull(matcherConfigurer, "MatcherConfigurer must not be empty!"); @@ -187,7 +192,7 @@ public ExampleSpec withMatcher(String propertyPath, MatcherConfigurer withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher) { + public ExampleSpec withMatcher(String propertyPath, GenericPropertyMatcher genericPropertyMatcher) { Assert.hasText(propertyPath, "PropertyPath must not be empty!"); Assert.notNull(genericPropertyMatcher, "GenericPropertyMatcher must not be empty!"); @@ -195,14 +200,21 @@ public ExampleSpec withMatcher(String propertyPath, GenericPropertyMatcher ge PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); PropertySpecifier propertySpecifier = new PropertySpecifier(propertyPath); - propertySpecifier.stringMatcher = genericPropertyMatcher.stringMatcher; - propertySpecifier.ignoreCase = genericPropertyMatcher.ignoreCase; - propertySpecifier.valueTransformer = genericPropertyMatcher.valueTransformer; + 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(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -213,20 +225,17 @@ public ExampleSpec withMatcher(String propertyPath, GenericPropertyMatcher ge * @param propertyValueTransformer must not be {@literal null}. * @return */ - public ExampleSpec withTransformer(String propertyPath, PropertyValueTransformer propertyValueTransformer) { + 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 = createOrClonePropertySpecifier(propertyPath, propertySpecifiers); + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); - propertySpecifier.valueTransformer = propertyValueTransformer; + propertySpecifiers.add(propertySpecifier.withValueTransformer(propertyValueTransformer)); - propertySpecifiers.add(propertySpecifier); - - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -236,7 +245,7 @@ public ExampleSpec withTransformer(String propertyPath, PropertyValueTransfor * @param propertyPaths must not be {@literal null} and not empty. * @return */ - public ExampleSpec withIgnoreCase(String... propertyPaths) { + public ExampleSpec withIgnoreCase(String... propertyPaths) { Assert.notEmpty(propertyPaths, "PropertyPaths must not be empty!"); Assert.noNullElements(propertyPaths, "PropertyPaths must not contain null elements!"); @@ -244,27 +253,20 @@ public ExampleSpec withIgnoreCase(String... propertyPaths) { PropertySpecifiers propertySpecifiers = new PropertySpecifiers(this.propertySpecifiers); for (String propertyPath : propertyPaths) { - PropertySpecifier propertySpecifier = createOrClonePropertySpecifier(propertyPath, propertySpecifiers); - propertySpecifier.ignoreCase = true; - propertySpecifiers.add(propertySpecifier); + PropertySpecifier propertySpecifier = getOrCreatePropertySpecifier(propertyPath, propertySpecifiers); + propertySpecifiers.add(propertySpecifier.withIgnoreCase(true)); } - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } - private PropertySpecifier createOrClonePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { - PropertySpecifier propertySpecifier; + protected PropertySpecifier getOrCreatePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) { if (propertySpecifiers.hasSpecifierForPath(propertyPath)) { - propertySpecifier = new PropertySpecifier(propertyPath); - PropertySpecifier existing = propertySpecifiers.getForPath(propertyPath); - propertySpecifier.ignoreCase = existing.ignoreCase; - propertySpecifier.stringMatcher = existing.stringMatcher; - } else { - propertySpecifier = new PropertySpecifier(propertyPath); + return propertySpecifiers.getForPath(propertyPath); } - return propertySpecifier; + + return new PropertySpecifier(propertyPath); } /** @@ -273,8 +275,8 @@ private PropertySpecifier createOrClonePropertySpecifier(String propertyPath, Pr * * @return */ - public ExampleSpec withIncludeNullValues() { - return new ExampleSpec(type, NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + public ExampleSpec withIncludeNullValues() { + return new ExampleSpec(NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } @@ -284,8 +286,8 @@ public ExampleSpec withIncludeNullValues() { * * @return */ - public ExampleSpec withIgnoreNullValues() { - return new ExampleSpec(type, NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + public ExampleSpec withIgnoreNullValues() { + return new ExampleSpec(NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } @@ -296,21 +298,10 @@ public ExampleSpec withIgnoreNullValues() { * @param nullHandler must not be {@literal null}. * @return */ - public ExampleSpec withNullHandler(NullHandler nullHandler) { + public ExampleSpec withNullHandler(NullHandler nullHandler) { Assert.notNull(nullHandler, "NullHandler must not be null!"); - return new ExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, - defaultIgnoreCase); - } - - /** - * Create a new {@link Example} containing the {@code probe}. - * - * @param probe must not be {@literal null}. - * @return - */ - public Example createExample(T probe) { - return Example.of(probe, this); + return new ExampleSpec(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase); } /** @@ -353,18 +344,11 @@ public Set getIgnoredPaths() { return ignoredPaths; } - PropertySpecifiers getPropertySpecifiers() { - return propertySpecifiers; - } - /** - * Create a new {@link ExampleSpec} including all non-null properties by default. - * - * @param type must not be {@literal null}. - * @return + * @return the {@link PropertySpecifiers} within the {@link ExampleSpec}. */ - public static ExampleSpec of(Class type) { - return new ExampleSpec(type); + public PropertySpecifiers getPropertySpecifiers() { + return propertySpecifiers; } /** @@ -394,9 +378,35 @@ public static interface MatcherConfigurer { */ public static class GenericPropertyMatcher { - private StringMatcher stringMatcher = null; - private Boolean ignoreCase = null; - private PropertyValueTransformer valueTransformer = NoOpPropertyValueTransformer.INSTANCE; + 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}. @@ -404,6 +414,7 @@ public static class GenericPropertyMatcher { * @return */ public GenericPropertyMatcher ignoreCase() { + this.ignoreCase = true; return this; } @@ -415,6 +426,7 @@ public GenericPropertyMatcher ignoreCase() { * @return */ public GenericPropertyMatcher ignoreCase(boolean ignoreCase) { + this.ignoreCase = ignoreCase; return this; } @@ -425,6 +437,7 @@ public GenericPropertyMatcher ignoreCase(boolean ignoreCase) { * @return */ public GenericPropertyMatcher caseSensitive() { + this.ignoreCase = false; return this; } @@ -435,6 +448,7 @@ public GenericPropertyMatcher caseSensitive() { * @return */ public GenericPropertyMatcher contains() { + this.stringMatcher = StringMatcher.CONTAINING; return this; } @@ -445,6 +459,7 @@ public GenericPropertyMatcher contains() { * @return */ public GenericPropertyMatcher endsWith() { + this.stringMatcher = StringMatcher.ENDING; return this; } @@ -455,6 +470,7 @@ public GenericPropertyMatcher endsWith() { * @return */ public GenericPropertyMatcher startsWith() { + this.stringMatcher = StringMatcher.STARTING; return this; } @@ -465,6 +481,7 @@ public GenericPropertyMatcher startsWith() { * @return */ public GenericPropertyMatcher exact() { + this.stringMatcher = StringMatcher.EXACT; return this; } @@ -475,6 +492,7 @@ public GenericPropertyMatcher exact() { * @return */ public GenericPropertyMatcher storeDefaultMatching() { + this.stringMatcher = StringMatcher.DEFAULT; return this; } @@ -485,6 +503,7 @@ public GenericPropertyMatcher storeDefaultMatching() { * @return */ public GenericPropertyMatcher regex() { + this.stringMatcher = StringMatcher.REGEX; return this; } @@ -496,6 +515,7 @@ public GenericPropertyMatcher regex() { * @return */ public GenericPropertyMatcher stringMatcher(StringMatcher stringMatcher) { + Assert.notNull(stringMatcher, "StringMatcher must not be null!"); this.stringMatcher = stringMatcher; return this; @@ -508,32 +528,12 @@ public GenericPropertyMatcher stringMatcher(StringMatcher stringMatcher) { * @return */ public GenericPropertyMatcher transform(PropertyValueTransformer propertyValueTransformer) { + Assert.notNull(propertyValueTransformer, "PropertyValueTransformer must not be null!"); this.valueTransformer = propertyValueTransformer; return this; } - /** - * 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); - } - } /** @@ -628,42 +628,27 @@ public static enum StringMatcher { /** * Store specific default. */ - DEFAULT(null), + DEFAULT, /** * Matches the exact string */ - EXACT(Type.SIMPLE_PROPERTY), + EXACT, /** * Matches string starting with pattern */ - STARTING(Type.STARTING_WITH), + STARTING, /** * Matches string ending with pattern */ - ENDING(Type.ENDING_WITH), + ENDING, /** * Matches string containing pattern */ - CONTAINING(Type.CONTAINING), + 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; - } + REGEX; } @@ -678,7 +663,7 @@ public static interface PropertyValueTransformer extends Converter propertySpecifiers = new LinkedHashMap(); - public PropertySpecifiers() {} + PropertySpecifiers() { + + } PropertySpecifiers(PropertySpecifiers propertySpecifiers) { this.propertySpecifiers.putAll(propertySpecifiers.propertySpecifiers); diff --git a/src/main/java/org/springframework/data/domain/TypedExampleSpec.java b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java new file mode 100644 index 0000000000..3689d7a210 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java @@ -0,0 +1,310 @@ +/* + * 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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore the {@code propertyPaths}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#STARTING}. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherStarting() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.STARTING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#ENDING}. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherEnding() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.ENDING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to + * {@link StringMatcher#CONTAINING}. + * + * @return + */ + @Override + public TypedExampleSpec withStringMatcherContaining() { + return new TypedExampleSpec(type, nullHandler, StringMatcher.CONTAINING, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set the {@code defaultStringMatcher}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with ignoring case sensitivity by + * default. + * + * @return + */ + @Override + public TypedExampleSpec withIgnoreCase() { + return withIgnoreCase(true); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with {@code defaultIgnoreCase}. + * + * @param defaultIgnoreCase + * @return + */ + @Override + public TypedExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { + return new TypedExampleSpec(type, nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a + * {@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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore case sensitivity for the + * {@code propertyPaths}. + * + * @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); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@link NullHandler#INCLUDE}. + * + * @return + */ + @Override + public TypedExampleSpec withIncludeNullValues() { + return new TypedExampleSpec(type, NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@link NullHandler#IGNORE}. + * + * @return + */ + @Override + public TypedExampleSpec withIgnoreNullValues() { + return new TypedExampleSpec(type, NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths, + defaultIgnoreCase); + } + + /** + * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} + * values to {@code nullHandler}. + * + * @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/domain/ExampleSpecAccessor.java b/src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java similarity index 91% rename from src/main/java/org/springframework/data/domain/ExampleSpecAccessor.java rename to src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java index 83cdfc0e1b..3afc2b90f0 100644 --- a/src/main/java/org/springframework/data/domain/ExampleSpecAccessor.java +++ b/src/main/java/org/springframework/data/repository/core/support/ExampleSpecAccessor.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.domain; +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. * @@ -25,9 +28,9 @@ */ public class ExampleSpecAccessor { - private final ExampleSpec exampleSpec; + private final ExampleSpec exampleSpec; - public ExampleSpecAccessor(ExampleSpec exampleSpec) { + public ExampleSpecAccessor(ExampleSpec exampleSpec) { this.exampleSpec = exampleSpec; } @@ -146,4 +149,11 @@ public ExampleSpec.PropertyValueTransformer getValueTransformerForPath(String pa 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 index 96f5279833..a5421e27f2 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/repository/query/QueryByExampleExecutor.java @@ -23,6 +23,7 @@ /** * Interface to allow execution of Query by Example {@link Example} instances. * + * @param * @author Mark Paluch * @since 1.12 */ @@ -31,41 +32,41 @@ 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}. + * @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. */ - T findOne(Example example); + 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}. + * @param example can be {@literal null}. * @return all entities matching the given {@link Example}. */ - Iterable findAll(Example 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 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); + 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 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); + Page findAll(Example example, Pageable pageable); /** * Returns the number of instances matching the given {@link Example}. diff --git a/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java index bd5df6ead2..fbd2fcca3b 100644 --- a/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java +++ b/src/test/java/org/springframework/data/domain/ExampleSpecUnitTests.java @@ -32,11 +32,11 @@ */ public class ExampleSpecUnitTests { - ExampleSpec exampleSpec; + ExampleSpec exampleSpec; @Before public void setUp() throws Exception { - exampleSpec = ExampleSpec.of(Person.class); + exampleSpec = ExampleSpec.untyped(); } /** @@ -53,7 +53,7 @@ public void defaultStringMatcherShouldReturnDefault() throws Exception { @Test public void defaultStringMatcherShouldReturnContainingWhenConfigured() throws Exception { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcherContaining(); + exampleSpec = ExampleSpec.untyped().withStringMatcherContaining(); assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.CONTAINING)); } @@ -63,7 +63,7 @@ public void defaultStringMatcherShouldReturnContainingWhenConfigured() throws Ex @Test public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exception { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcherStarting(); + exampleSpec = ExampleSpec.untyped().withStringMatcherStarting(); assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.STARTING)); } @@ -73,7 +73,7 @@ public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exce @Test public void defaultStringMatcherShouldReturnEndingWhenConfigured() throws Exception { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcherEnding(); + exampleSpec = ExampleSpec.untyped().withStringMatcherEnding(); assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.ENDING)); } @@ -114,7 +114,7 @@ public void ignoredPathsIsNotModifiable() throws Exception { */ @Test(expected = IllegalArgumentException.class) public void defaultExampleSpecWithoutTypeFails() throws Exception { - ExampleSpec.of(null); + ExampleSpec.typed(null); } /** @@ -123,7 +123,7 @@ public void defaultExampleSpecWithoutTypeFails() throws Exception { @Test public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase(); + exampleSpec = ExampleSpec.untyped().withIgnoreCase(); assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); } @@ -134,7 +134,7 @@ public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { @Test public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase(true); + exampleSpec = ExampleSpec.untyped().withIgnoreCase(true); assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); } @@ -145,7 +145,7 @@ public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { @Test public void nullHandlerShouldReturnInclude() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIncludeNullValues(); + exampleSpec = ExampleSpec.untyped().withIncludeNullValues(); assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); } @@ -156,7 +156,7 @@ public void nullHandlerShouldReturnInclude() throws Exception { @Test public void nullHandlerShouldReturnIgnore() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnoreNullValues(); + exampleSpec = ExampleSpec.untyped().withIgnoreNullValues(); assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); } @@ -167,7 +167,7 @@ public void nullHandlerShouldReturnIgnore() throws Exception { @Test public void nullHandlerShouldReturnConfiguredValue() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withNullHandler(NullHandler.INCLUDE); + exampleSpec = ExampleSpec.untyped().withNullHandler(NullHandler.INCLUDE); assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); } @@ -178,7 +178,7 @@ public void nullHandlerShouldReturnConfiguredValue() throws Exception { @Test public void ignoredPathsShouldReturnCorrectProperties() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnorePaths("foo", "bar", "baz"); + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "baz"); assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar", "baz")); assertThat(exampleSpec.getIgnoredPaths(), hasSize(3)); @@ -190,7 +190,7 @@ public void ignoredPathsShouldReturnCorrectProperties() throws Exception { @Test public void ignoredPathsShouldReturnUniqueProperties() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnorePaths("foo", "bar", "foo"); + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "foo"); assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar")); assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); @@ -202,8 +202,8 @@ public void ignoredPathsShouldReturnUniqueProperties() throws Exception { @Test public void withCreatesNewInstance() throws Exception { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withIgnorePaths("foo", "bar", "foo"); - ExampleSpec configuredExampleSpec = exampleSpec.withIgnoreCase(); + exampleSpec = ExampleSpec.untyped().withIgnorePaths("foo", "bar", "foo"); + ExampleSpec configuredExampleSpec = exampleSpec.withIgnoreCase(); assertThat(exampleSpec, is(not(sameInstance(configuredExampleSpec)))); assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); diff --git a/src/test/java/org/springframework/data/domain/ExampleUnitTests.java b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java index aeaa352ae1..8cff9970e6 100644 --- a/src/test/java/org/springframework/data/domain/ExampleUnitTests.java +++ b/src/test/java/org/springframework/data/domain/ExampleUnitTests.java @@ -47,7 +47,7 @@ public void setUp() { */ @Test(expected = IllegalArgumentException.class) public void exampleOfNullThrowsException() { - new Example(null); + Example.of(null); } /** @@ -58,6 +58,37 @@ public void getSampleTypeRetunsSampleObjectsClass() { assertThat(example.getProbeType(), equalTo(Person.class)); } + /** + * @see DATACMNS-810 + */ + @Test + public void createTypedExample() { + + Example example = of(new Person(), ExampleSpec.typed(Contact.class)); + + assertThat(example.getProbeType(), equalTo(Person.class)); + assertThat(example.getResultType(), equalTo((Class) Contact.class)); + assertThat(example.getProbeType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void createUntypedExample() { + + Example example = of(new Person(), ExampleSpec.untyped()); + + assertThat(example.getProbeType(), equalTo(Person.class)); + assertThat(example.getResultType(), equalTo((Class) Person.class)); + assertThat(example.getProbeType(), equalTo(Person.class)); + } + + static class Contact { + + String label; + } + static class Person { String firstname; diff --git a/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java b/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java new file mode 100644 index 0000000000..aea31209ad --- /dev/null +++ b/src/test/java/org/springframework/data/domain/TypedExampleSpecUnitTests.java @@ -0,0 +1,231 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.StringMatcher; + +/** + * Unit test for {@link ExampleSpec}. + * + * @author Mark Paluch + * @soundtrack Harmonic Rush - The Dark Side of Persia (ahmed romel remix) + */ +public class TypedExampleSpecUnitTests { + + TypedExampleSpec exampleSpec; + + @Before + public void setUp() throws Exception { + exampleSpec = ExampleSpec.typed(Person.class); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void typeShouldReturnConfiguredType() throws Exception { + assertThat(exampleSpec.getType(), equalTo(Person.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() throws Exception { + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnContainingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherContaining(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnStartingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherStarting(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnEndingWhenConfigured() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcherEnding(); + assertThat(exampleSpec.getDefaultStringMatcher(), is(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() throws Exception { + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsIsEmptyByDefault() throws Exception { + assertThat(exampleSpec.getIgnoredPaths(), is(empty())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnoreByDefault() throws Exception { + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = UnsupportedOperationException.class) + public void ignoredPathsIsNotModifiable() throws Exception { + exampleSpec.getIgnoredPaths().add("¯\\_(ツ)_/¯"); + } + + /** + * @see DATACMNS-810 + */ + @Test(expected = IllegalArgumentException.class) + public void defaultExampleSpecWithoutTypeFails() throws Exception { + ExampleSpec.typed(null); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseEnabled() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseSet() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(true); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIncludeNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnIgnore() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreNullValues(); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.IGNORE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnConfiguredValue() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withNullHandler(NullHandler.INCLUDE); + + assertThat(exampleSpec.getNullHandler(), is(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnCorrectProperties() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "baz"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar", "baz")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(3)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoredPathsShouldReturnUniqueProperties() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "foo"); + + assertThat(exampleSpec.getIgnoredPaths(), contains("foo", "bar")); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void withCreatesNewInstance() throws Exception { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("foo", "bar", "foo"); + TypedExampleSpec configuredExampleSpec = exampleSpec.withIgnoreCase(); + + assertThat(exampleSpec, is(not(sameInstance(configuredExampleSpec)))); + assertThat(exampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + + assertThat(configuredExampleSpec.getIgnoredPaths(), hasSize(2)); + assertThat(configuredExampleSpec.isIgnoreCaseEnabled(), is(true)); + } + + static class Person { + + String firstname; + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java new file mode 100644 index 0000000000..195d6fb22a --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorTypedExampleSpecUnitTests.java @@ -0,0 +1,315 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.core.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatcher; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatchers; +import org.springframework.data.domain.ExampleSpec.MatcherConfigurer; +import org.springframework.data.domain.ExampleSpec.NoOpPropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.NullHandler; +import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; +import org.springframework.data.domain.ExampleSpec.StringMatcher; +import org.springframework.data.domain.TypedExampleSpec; + +/** + * Test for {@link ExampleSpecAccessor}. + * + * @author Mark Paluch + * @soundtrack Cabballero - Dancing With Tears In My Eyes (Dance Maxi) + */ +public class ExampleSpecAccessorTypedExampleSpecUnitTests { + + private Person person; + private TypedExampleSpec exampleSpec; + private ExampleSpecAccessor exampleSpecAccessor; + + @Before + public void setUp() { + + person = new Person(); + person.firstname = "rand"; + + exampleSpec = ExampleSpec.typed(Person.class); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void isTypedShouldReturnTrue() { + assertThat(exampleSpecAccessor.isTyped(), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void defaultStringMatcherShouldReturnDefault() { + assertThat(exampleSpecAccessor.getDefaultStringMatcher(), equalTo(StringMatcher.DEFAULT)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void nullHandlerShouldReturnInclude() { + + exampleSpec = ExampleSpec.typed(Person.class).withIncludeNullValues(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getNullHandler(), equalTo(NullHandler.INCLUDE)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldIgnorePaths() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnorePaths("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoredPath("firstname"), equalTo(true)); + assertThat(exampleSpecAccessor.isIgnoredPath("lastname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatDoesNotHavePropertySpecifier() { + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), + equalTo(exampleSpec.getDefaultStringMatcher())); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHavePropertySpecifier() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.CONTAINING); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseConfiguredIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase().withMatcher("firstname", + new GenericPropertyMatcher().contains().caseSensitive()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), equalTo(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherStarting() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.startsWith()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherContaining() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherRegex() { + + exampleSpec = ExampleSpec.typed(Person.class).withMatcher("firstname", GenericPropertyMatchers.regex()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.REGEX)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldFavorStringMatcherDefinedForPathOverConfiguredDefaultStringMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.ENDING) + .withMatcher("firstname", new GenericPropertyMatcher().contains()) + .withMatcher("address.city", new GenericPropertyMatcher().startsWith()) + .withMatcher("lastname", new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getPropertySpecifiers(), hasSize(3)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("lastname"), equalTo(StringMatcher.ENDING)); + assertThat(exampleSpecAccessor.getStringMatcherForPath("unknownProperty"), equalTo(StringMatcher.ENDING)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void exampleShouldUseDefaultStringMatcherForPathThatHasPropertySpecifierWithoutStringMatcher() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new MatcherConfigurer() { + @Override + public void configureMatcher(GenericPropertyMatcher matcher) { + matcher.ignoreCase(); + } + }); + + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("unknownProperty"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnFalseByDefault() { + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase(); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.isIgnoreCaseEnabled(), is(true)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void ignoreCaseShouldFavorPathSpecificSettings() { + + exampleSpec = ExampleSpec.typed(Person.class).withIgnoreCase("firstname"); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); + assertThat(exampleSpecAccessor.isIgnoreCaseForPath("firstname"), is(true)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsNoOpValueTransformerByDefault() { + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), + instanceOf(NoOpPropertyValueTransformer.class)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void getValueTransformerForPathReturnsConfigurtedTransformerForPath() { + + PropertyValueTransformer transformer = new PropertyValueTransformer() { + + @Override + public Object convert(Object source) { + return source.toString(); + } + }; + + exampleSpec = ExampleSpec.typed(Person.class).withTransformer("firstname", transformer); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), equalTo(transformer)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsFalseIfNoneDefined() { + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(false)); + } + + /** + * @see DATACMNS-810 + */ + @Test + public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { + + exampleSpec = ExampleSpec.typed(Person.class).withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new GenericPropertyMatcher().contains()); + exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); + + assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(true)); + } + + static class Person { + + String firstname; + } + +} diff --git a/src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java similarity index 83% rename from src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java rename to src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java index a36894c456..c7536b93a2 100644 --- a/src/test/java/org/springframework/data/domain/ExampleSpecAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecAccessorUntypedExampleSpecUnitTests.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.domain; +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; @@ -32,12 +33,12 @@ * Test for {@link ExampleSpecAccessor}. * * @author Mark Paluch - * @soundtrack Cabballero - Dancing With Tears In My Eyes (Dance Maxi) + * @soundtrack Alex Wackii and Tolga Uzulmez - Diamond Rush (original mix) */ -public class ExampleSpecAccessorUnitTests { +public class ExampleSpecAccessorUntypedExampleSpecUnitTests { private Person person; - private ExampleSpec exampleSpec; + private ExampleSpec exampleSpec; private ExampleSpecAccessor exampleSpecAccessor; @Before @@ -46,7 +47,7 @@ public void setUp() { person = new Person(); person.firstname = "rand"; - exampleSpec = ExampleSpec.of(Person.class); + exampleSpec = ExampleSpec.untyped(); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); } @@ -64,7 +65,7 @@ public void defaultStringMatcherShouldReturnDefault() { @Test public void nullHandlerShouldReturnInclude() { - exampleSpec = ExampleSpec.of(Person.class).withIncludeNullValues(); + exampleSpec = ExampleSpec.untyped().withIncludeNullValues(); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getNullHandler(), equalTo(NullHandler.INCLUDE)); @@ -76,7 +77,7 @@ public void nullHandlerShouldReturnInclude() { @Test public void exampleShouldIgnorePaths() { - exampleSpec = ExampleSpec.of(Person.class).withIgnorePaths("firstname"); + exampleSpec = ExampleSpec.untyped().withIgnorePaths("firstname"); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.isIgnoredPath("firstname"), equalTo(true)); @@ -98,7 +99,7 @@ public void exampleShouldUseDefaultStringMatcherForPathThatDoesNotHavePropertySp @Test public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHavePropertySpecifier() { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.CONTAINING); + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.CONTAINING); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); @@ -110,7 +111,7 @@ public void exampleShouldUseConfiguredStringMatcherAsDefaultForPathThatDoesNotHa @Test public void exampleShouldUseDefaultIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { - exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase().withMatcher("firstname", + exampleSpec = ExampleSpec.untyped().withIgnoreCase().withMatcher("firstname", new GenericPropertyMatcher().contains()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); @@ -123,7 +124,7 @@ public void exampleShouldUseDefaultIgnoreCaseForPathThatDoesHavePropertySpecifie @Test public void exampleShouldUseConfiguredIgnoreCaseForPathThatDoesHavePropertySpecifierWithMatcher() { - exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase().withMatcher("firstname", + exampleSpec = ExampleSpec.untyped().withIgnoreCase().withMatcher("firstname", new GenericPropertyMatcher().contains().caseSensitive()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); @@ -136,7 +137,7 @@ public void exampleShouldUseConfiguredIgnoreCaseForPathThatDoesHavePropertySpeci @Test public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherStarting() { - exampleSpec = ExampleSpec.of(Person.class).withMatcher("firstname", GenericPropertyMatchers.startsWith()); + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.startsWith()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.STARTING)); @@ -148,7 +149,7 @@ public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpeci @Test public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherContaining() { - exampleSpec = ExampleSpec.of(Person.class).withMatcher("firstname", GenericPropertyMatchers.contains()); + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.contains()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.CONTAINING)); @@ -160,7 +161,7 @@ public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpeci @Test public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpecifierWithStringMatcherRegex() { - exampleSpec = ExampleSpec.of(Person.class).withMatcher("firstname", GenericPropertyMatchers.regex()); + exampleSpec = ExampleSpec.untyped().withMatcher("firstname", GenericPropertyMatchers.regex()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getStringMatcherForPath("firstname"), equalTo(StringMatcher.REGEX)); @@ -172,7 +173,7 @@ public void exampleShouldUseDefinedStringMatcherForPathThatDoesHavePropertySpeci @Test public void exampleShouldFavorStringMatcherDefinedForPathOverConfiguredDefaultStringMatcher() { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.ENDING) + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING) .withMatcher("firstname", new GenericPropertyMatcher().contains()) .withMatcher("address.city", new GenericPropertyMatcher().startsWith()) .withMatcher("lastname", new MatcherConfigurer() { @@ -196,7 +197,7 @@ public void configureMatcher(GenericPropertyMatcher matcher) { @Test public void exampleShouldUseDefaultStringMatcherForPathThatHasPropertySpecifierWithoutStringMatcher() { - exampleSpec = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", new MatcherConfigurer() { @Override public void configureMatcher(GenericPropertyMatcher matcher) { @@ -227,7 +228,7 @@ public void ignoreCaseShouldReturnFalseByDefault() { @Test public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { - exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase(); + exampleSpec = ExampleSpec.untyped().withIgnoreCase(); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.isIgnoreCaseEnabled(), is(true)); @@ -240,7 +241,7 @@ public void ignoreCaseShouldReturnTrueWhenIgnoreCaseIsEnabled() { @Test public void ignoreCaseShouldFavorPathSpecificSettings() { - exampleSpec = ExampleSpec.of(Person.class).withIgnoreCase("firstname"); + exampleSpec = ExampleSpec.untyped().withIgnoreCase("firstname"); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpec.isIgnoreCaseEnabled(), is(false)); @@ -270,7 +271,7 @@ public Object convert(Object source) { } }; - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withTransformer("firstname", transformer); + exampleSpec = ExampleSpec.untyped().withTransformer("firstname", transformer); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.getValueTransformerForPath("firstname"), equalTo(transformer)); @@ -290,8 +291,8 @@ public void hasPropertySpecifiersReturnsFalseIfNoneDefined() { @Test public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { - ExampleSpec exampleSpec = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.STARTING) - .withMatcher("firstname", new GenericPropertyMatcher().contains()); + exampleSpec = ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING).withMatcher("firstname", + new GenericPropertyMatcher().contains()); exampleSpecAccessor = new ExampleSpecAccessor(exampleSpec); assertThat(exampleSpecAccessor.hasPropertySpecifiers(), is(true)); From effabb19c44df34f1b39804636d432da891e074e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 11:36:15 +0100 Subject: [PATCH 6/6] DATACMNS-810 - Update documentation. --- src/main/asciidoc/query-by-example.adoc | 94 +++++++++++++------ .../springframework/data/domain/Example.java | 10 +- .../data/domain/ExampleSpec.java | 71 ++++++++------ .../data/domain/TypedExampleSpec.java | 53 ++++++----- 4 files changed, 143 insertions(+), 85 deletions(-) diff --git a/src/main/asciidoc/query-by-example.adoc b/src/main/asciidoc/query-by-example.adoc index 846e187a11..1c07fb3a38 100644 --- a/src/main/asciidoc/query-by-example.adoc +++ b/src/main/asciidoc/query-by-example.adoc @@ -1,18 +1,18 @@ [[query.by.example]] -= Query by Example +== Query by Example -== Introduction +=== 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 +=== 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`: 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: @@ -48,7 +48,7 @@ public class Person { ---- ==== -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 <>. Once the `Example` is constructed it is immutable. +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 ==== @@ -75,9 +75,9 @@ Examples can be executed ideally with Repositories. To do so, let your repositor ---- public interface QueryByExampleExecutor { - T findOne(Example example); + S findOne(Example example); - Iterable findAll(Example example); + Iterable findAll(Example example); // … more functionality omitted. } @@ -87,39 +87,39 @@ public interface QueryByExampleExecutor { You can read more about <> below. [[query.by.example.examplespec]] -== Example Spec +=== 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`. +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. -.Example Spec with customized matching +.Untyped Example Spec with customized matching ==== [source,java] ---- -Person person = new Person(); <1> +Person person = new Person(); <1> -person.setFirstname("Dave"); <2> +person.setFirstname("Dave"); <2> -ExampleSpec exampleSpec = ExampleSpec.of(Person.class) <3> +ExampleSpec exampleSpec = ExampleSpec.untyped() <3> - .withIgnorePaths("lastname") <4> + .withIgnorePaths("lastname") <4> - .withIncludeNullValues() <5> + .withIncludeNullValues() <5> - .withStringMatcherEnding(); <6> + .withStringMatcherEnding(); <6> -Example example = Example.of(person, exampleSpec); <7> +Example example = Example.of(person, exampleSpec); <7> ---- -<1> Create a new instance of the domain object -<2> Set properties -<3> Create an `ExampleSpec` for the `Person` type. 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` +<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 once it is created. Calls to `with…(…)` create each time a new `ExampleSpec` that carry all settings from the originating instance and setting the specific configuration of the `with…(…)` so 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. +`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. @@ -127,7 +127,7 @@ You can specify behavior for individual properties (e.g. "firstname" and "lastna ==== [source,java] ---- -ExampleSpec example = ExampleSpec.of(Person.class) +ExampleSpec exampleSpec = ExampleSpec.untyped() .withMatcher("firstname", endsWith()) .withMatcher("lastname", startsWith().ignoreCase()); } @@ -140,7 +140,7 @@ Another style to configure matcher options is by using Java 8 lambdas. This appr ==== [source,java] ---- -ExampleSpec example = ExampleSpec.of(Person.class) +ExampleSpec exampleSpec = ExampleSpec.untyped() .withMatcher("firstname", matcher -> matcher.endsWith()) .withMatcher("firstname", matcher -> matcher.startsWith()); } @@ -171,3 +171,43 @@ Queries created by `Example` use a merged view of the configuration. Default mat | 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 4e7d14b410..3fa95c20e6 100644 --- a/src/main/java/org/springframework/data/domain/Example.java +++ b/src/main/java/org/springframework/data/domain/Example.java @@ -19,7 +19,11 @@ 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 @@ -60,7 +64,7 @@ private Example(T probe, ExampleSpec exampleSpec) { } /** - * Create a new {@link Example} including all non-null properties by default. + * 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 @@ -70,7 +74,7 @@ public static Example of(T probe) { } /** - * Create a new {@link Example} with a configured {@link ExampleSpec}. + * 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}. diff --git a/src/main/java/org/springframework/data/domain/ExampleSpec.java b/src/main/java/org/springframework/data/domain/ExampleSpec.java index 9284e08f3d..9d8051fabc 100644 --- a/src/main/java/org/springframework/data/domain/ExampleSpec.java +++ b/src/main/java/org/springframework/data/domain/ExampleSpec.java @@ -28,10 +28,12 @@ /** * 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} but they can be refined using the various - * {@code with...} methods in a fluent style. A {@code with...} method creates a new instance of {@link ExampleSpec} - * containing all settings from the current instance but sets the value in the new instance. Null-handling defaults to + * 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 @@ -66,7 +68,7 @@ public class ExampleSpec { } /** - * Create a new {@link ExampleSpec} including all non-null properties by default. + * Create a new untyped {@link ExampleSpec} including all non-null properties by default. * * @param type must not be {@literal null}. * @return @@ -76,7 +78,7 @@ public static ExampleSpec untyped() { } /** - * Create a new {@link ExampleSpec} including all non-null properties by default. + * Create a new {@link TypedExampleSpec} including all non-null properties by default. * * @param type must not be {@literal null}. * @return @@ -86,7 +88,8 @@ public static TypedExampleSpec typed(Class type) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore the {@code propertyPaths}. + * 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 @@ -103,8 +106,8 @@ public ExampleSpec withIgnorePaths(String... ignoredPaths) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#STARTING}. + * 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 */ @@ -113,8 +116,8 @@ public ExampleSpec withStringMatcherStarting() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#ENDING}. + * 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 */ @@ -123,8 +126,8 @@ public ExampleSpec withStringMatcherEnding() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#CONTAINING}. + * 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 */ @@ -133,7 +136,8 @@ public ExampleSpec withStringMatcherContaining() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set the {@code defaultStringMatcher}. + * 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 @@ -146,8 +150,8 @@ public ExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with ignoring case sensitivity by - * default. + * Returns a copy of this {@link ExampleSpec} with ignoring case sensitivity by default. This instance is immutable + * and unaffected by this method call. * * @return */ @@ -156,7 +160,8 @@ public ExampleSpec withIgnoreCase() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with {@code defaultIgnoreCase}. + * Returns a copy of this {@link ExampleSpec} with {@code defaultIgnoreCase}. This instance is immutable and + * unaffected by this method call. * * @param defaultIgnoreCase * @return @@ -166,8 +171,8 @@ public ExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a - * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * 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}. @@ -185,8 +190,8 @@ public ExampleSpec withMatcher(String propertyPath, MatcherConfigurer propertySpecifiers = new LinkedHashMap(); diff --git a/src/main/java/org/springframework/data/domain/TypedExampleSpec.java b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java index 3689d7a210..1fb17b19ad 100644 --- a/src/main/java/org/springframework/data/domain/TypedExampleSpec.java +++ b/src/main/java/org/springframework/data/domain/TypedExampleSpec.java @@ -61,7 +61,8 @@ public static TypedExampleSpec of(Class type) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore the {@code propertyPaths}. + * 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 @@ -80,8 +81,8 @@ public TypedExampleSpec withIgnorePaths(String... ignoredPaths) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#STARTING}. + * 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 */ @@ -92,8 +93,8 @@ public TypedExampleSpec withStringMatcherStarting() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#ENDING}. + * 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 */ @@ -104,8 +105,8 @@ public TypedExampleSpec withStringMatcherEnding() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set string matching to - * {@link StringMatcher#CONTAINING}. + * 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 */ @@ -116,7 +117,8 @@ public TypedExampleSpec withStringMatcherContaining() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set the {@code defaultStringMatcher}. + * 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 @@ -131,8 +133,8 @@ public TypedExampleSpec withStringMatcher(StringMatcher defaultStringMatcher) } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with ignoring case sensitivity by - * default. + * Returns a copy of this {@link TypedExampleSpec} with ignoring case sensitivity by default. This instance is + * immutable and unaffected by this method call. * * @return */ @@ -142,7 +144,8 @@ public TypedExampleSpec withIgnoreCase() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} with {@code defaultIgnoreCase}. + * Returns a copy of this {@link TypedExampleSpec} with {@code defaultIgnoreCase}. This instance is immutable and + * unaffected by this method call. * * @param defaultIgnoreCase * @return @@ -154,8 +157,8 @@ public TypedExampleSpec withIgnoreCase(boolean defaultIgnoreCase) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a - * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * 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}. @@ -175,8 +178,8 @@ public TypedExampleSpec withMatcher(String propertyPath, } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a - * {@code GenericPropertyMatcher} for the {@code propertyPath}. + * 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}. @@ -210,8 +213,8 @@ public TypedExampleSpec withMatcher(String propertyPath, GenericPropertyMatch } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and configure a - * {@code PropertyValueTransformer} for the {@code propertyPath}. + * 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}. @@ -233,8 +236,8 @@ public TypedExampleSpec withTransformer(String propertyPath, PropertyValueTra } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and ignore case sensitivity for the - * {@code propertyPaths}. + * 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 @@ -257,8 +260,8 @@ public TypedExampleSpec withIgnoreCase(String... propertyPaths) { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} - * values to {@link NullHandler#INCLUDE}. + * 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 */ @@ -269,8 +272,8 @@ public TypedExampleSpec withIncludeNullValues() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} - * values to {@link NullHandler#IGNORE}. + * 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 */ @@ -281,8 +284,8 @@ public TypedExampleSpec withIgnoreNullValues() { } /** - * Create a new {@link ExampleSpec} based on the current {@link ExampleSpec} and set treatment of {@literal null} - * values to {@code nullHandler}. + * 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