/*
* Copyright 2016-2017 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 lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
import lombok.experimental.Wither;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.Assert;
/**
* Specification for property path matching to use in query by example (QBE). An {@link ExampleMatcher} can be created
* for a {@link Class object type}. Instances of {@link ExampleMatcher} can be either {@link #matching()} 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 ExampleMatcher} instance with the specified setting. Null-handling defaults to
* {@link NullHandler#IGNORE} and case-sensitive {@link StringMatcher#DEFAULT} string matching.
* <p>
* This class is immutable.
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Oliver Gierke
* @param <T>
* @since 1.12
*/
@ToString
@EqualsAndHashCode
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class ExampleMatcher {
NullHandler nullHandler;
StringMatcher defaultStringMatcher;
PropertySpecifiers propertySpecifiers;
Set<String> ignoredPaths;
boolean defaultIgnoreCase;
@Wither(AccessLevel.PRIVATE) MatchMode mode;
private ExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false,
MatchMode.ALL);
}
/**
* Create a new {@link ExampleMatcher} including all non-null properties by default exposing that all resulting
* predicates are supposed to be AND-concatenated.
*
* @param type will never be {@literal null}.
* @return
* @see #matchingAll()
*/
public static ExampleMatcher matching() {
return matchingAll();
}
/**
* Create a new {@link ExampleMatcher} including all non-null properties by default matching any predicate derived
* from the example.
*
* @param type will never be {@literal null}.
* @return
*/
public static ExampleMatcher matchingAny() {
return new ExampleMatcher().withMode(MatchMode.ANY);
}
/**
* Create a new {@link ExampleMatcher} including all non-null properties by default matching all predicates derived
* from the example.
*
* @param type will never be {@literal null}.
* @return
*/
public static ExampleMatcher matchingAll() {
return new ExampleMatcher().withMode(MatchMode.ALL);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified {@code propertyPaths}. This instance is immutable
* and unaffected by this method call.
*
* @param ignoredPaths must not be {@literal null} and not empty.
* @return
*/
public ExampleMatcher withIgnorePaths(String... ignoredPaths) {
Assert.notEmpty(ignoredPaths, "IgnoredPaths must not be empty!");
Assert.noNullElements(ignoredPaths, "IgnoredPaths must not contain null elements!");
Set<String> newIgnoredPaths = new LinkedHashSet<>(this.ignoredPaths);
newIgnoredPaths.addAll(Arrays.asList(ignoredPaths));
return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths, defaultIgnoreCase,
mode);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified string matching of {@code defaultStringMatcher}.
* This instance is immutable and unaffected by this method call.
*
* @param defaultStringMatcher must not be {@literal null}.
* @return
*/
public ExampleMatcher withStringMatcher(StringMatcher defaultStringMatcher) {
Assert.notNull(ignoredPaths, "DefaultStringMatcher must not be empty!");
return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
/**
* Returns a copy of this {@link ExampleMatcher} with ignoring case sensitivity by default. This instance is immutable
* and unaffected by this method call.
*
* @return
*/
public ExampleMatcher withIgnoreCase() {
return withIgnoreCase(true);
}
/**
* Returns a copy of this {@link ExampleMatcher} with {@code defaultIgnoreCase}. This instance is immutable and
* unaffected by this method call.
*
* @param defaultIgnoreCase
* @return
*/
public ExampleMatcher withIgnoreCase(boolean defaultIgnoreCase) {
return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified {@code GenericPropertyMatcher} for the
* {@code propertyPath}. This instance is immutable and unaffected by this method call.
*
* @param propertyPath must not be {@literal null}.
* @param matcherConfigurer callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}.
* @return
*/
public ExampleMatcher withMatcher(String propertyPath, MatcherConfigurer<GenericPropertyMatcher> matcherConfigurer) {
Assert.hasText(propertyPath, "PropertyPath must not be empty!");
Assert.notNull(matcherConfigurer, "MatcherConfigurer must not be empty!");
GenericPropertyMatcher genericPropertyMatcher = new GenericPropertyMatcher();
matcherConfigurer.configureMatcher(genericPropertyMatcher);
return withMatcher(propertyPath, genericPropertyMatcher);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified {@code GenericPropertyMatcher} for the
* {@code propertyPath}. This instance is immutable and unaffected by this method call.
*
* @param propertyPath must not be {@literal null}.
* @param genericPropertyMatcher callback to configure a {@link GenericPropertyMatcher}, must not be {@literal null}.
* @return
*/
public ExampleMatcher 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 ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified {@code PropertyValueTransformer} for the
* {@code propertyPath}.
*
* @param propertyPath must not be {@literal null}.
* @param propertyValueTransformer must not be {@literal null}.
* @return
*/
public ExampleMatcher 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 ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
/**
* Returns a copy of this {@link ExampleMatcher} with ignore case sensitivity for the {@code propertyPaths}. This
* instance is immutable and unaffected by this method call.
*
* @param propertyPaths must not be {@literal null} and not empty.
* @return
*/
public ExampleMatcher 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 ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
private PropertySpecifier getOrCreatePropertySpecifier(String propertyPath, PropertySpecifiers propertySpecifiers) {
if (propertySpecifiers.hasSpecifierForPath(propertyPath)) {
return propertySpecifiers.getForPath(propertyPath);
}
return new PropertySpecifier(propertyPath);
}
/**
* Returns a copy of this {@link ExampleMatcher} with treatment for {@literal null} values of
* {@link NullHandler#INCLUDE} . This instance is immutable and unaffected by this method call.
*
* @return
*/
public ExampleMatcher withIncludeNullValues() {
return withNullHandler(NullHandler.INCLUDE);
}
/**
* Returns a copy of this {@link ExampleMatcher} with treatment for {@literal null} values of
* {@link NullHandler#IGNORE}. This instance is immutable and unaffected by this method call.
*
* @return
*/
public ExampleMatcher withIgnoreNullValues() {
return withNullHandler(NullHandler.IGNORE);
}
/**
* Returns a copy of this {@link ExampleMatcher} with the specified {@code nullHandler}. This instance is immutable
* and unaffected by this method call.
*
* @param nullHandler must not be {@literal null}.
* @return
*/
public ExampleMatcher withNullHandler(NullHandler nullHandler) {
Assert.notNull(nullHandler, "NullHandler must not be null!");
return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, ignoredPaths, defaultIgnoreCase,
mode);
}
/**
* Get defined null handling.
*
* @return never {@literal null}
*/
public ExampleMatcher.NullHandler getNullHandler() {
return nullHandler;
}
/**
* Get defined {@link ExampleMatcher.StringMatcher}.
*
* @return never {@literal null}.
*/
public ExampleMatcher.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<String> getIgnoredPaths() {
return ignoredPaths;
}
/**
* @return the {@link PropertySpecifiers} within the {@link ExampleMatcher}.
*/
public PropertySpecifiers getPropertySpecifiers() {
return propertySpecifiers;
}
/**
* Returns whether all of the predicates of the {@link Example} are supposed to match. If {@literal false} is
* returned, it's sufficient if any of the predicates derived from the {@link Example} match.
*
* @return whether all of the predicates of the {@link Example} are supposed to match or any of them is sufficient.
*/
public boolean isAllMatching() {
return mode.equals(MatchMode.ALL);
}
/**
* Returns whether it's sufficient that any of the predicates of the {@link Example} match. If {@literal false} is
* returned, all predicates derived from the example need to match to produce results.
*
* @return whether it's sufficient that any of the predicates of the {@link Example} match or all need to match.
*/
public boolean isAnyMatching() {
return mode.equals(MatchMode.ANY);
}
/**
* Null handling for creating criterion out of an {@link Example}.
*
* @author Christoph Strobl
*/
public static enum NullHandler {
INCLUDE, IGNORE
}
/**
* Callback to configure a matcher.
*
* @author Mark Paluch
* @param <T>
*/
public static interface MatcherConfigurer<T> {
void configureMatcher(T matcher);
}
/**
* A generic property matcher that specifies {@link StringMatcher string matching} and case sensitivity.
*
* @author Mark Paluch
*/
@EqualsAndHashCode
public static class GenericPropertyMatcher {
StringMatcher stringMatcher = null;
Boolean ignoreCase = null;
PropertyValueTransformer valueTransformer = NoOpPropertyValueTransformer.INSTANCE;
/**
* Creates an unconfigured {@link GenericPropertyMatcher}.
*/
public GenericPropertyMatcher() {}
/**
* Creates a new {@link GenericPropertyMatcher} with a {@link StringMatcher} and {@code ignoreCase}.
*
* @param stringMatcher must not be {@literal null}.
* @param ignoreCase
* @return
*/
public static GenericPropertyMatcher of(StringMatcher stringMatcher, boolean ignoreCase) {
return new GenericPropertyMatcher().stringMatcher(stringMatcher).ignoreCase(ignoreCase);
}
/**
* Creates a new {@link GenericPropertyMatcher} with a {@link StringMatcher} and {@code ignoreCase}.
*
* @param stringMatcher must not be {@literal null}.
* @return
*/
public static GenericPropertyMatcher of(StringMatcher stringMatcher) {
return new GenericPropertyMatcher().stringMatcher(stringMatcher);
}
/**
* Sets ignores case to {@literal true}.
*
* @return
*/
public GenericPropertyMatcher ignoreCase() {
this.ignoreCase = true;
return this;
}
/**
* Sets ignores case to {@code ignoreCase}.
*
* @param ignoreCase
* @return
*/
public GenericPropertyMatcher ignoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
return this;
}
/**
* Sets ignores case to {@literal false}.
*
* @return
*/
public GenericPropertyMatcher caseSensitive() {
this.ignoreCase = false;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#CONTAINING}.
*
* @return
*/
public GenericPropertyMatcher contains() {
this.stringMatcher = StringMatcher.CONTAINING;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#ENDING}.
*
* @return
*/
public GenericPropertyMatcher endsWith() {
this.stringMatcher = StringMatcher.ENDING;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#STARTING}.
*
* @return
*/
public GenericPropertyMatcher startsWith() {
this.stringMatcher = StringMatcher.STARTING;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#EXACT}.
*
* @return
*/
public GenericPropertyMatcher exact() {
this.stringMatcher = StringMatcher.EXACT;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#DEFAULT}.
*
* @return
*/
public GenericPropertyMatcher storeDefaultMatching() {
this.stringMatcher = StringMatcher.DEFAULT;
return this;
}
/**
* Sets string matcher to {@link StringMatcher#REGEX}.
*
* @return
*/
public GenericPropertyMatcher regex() {
this.stringMatcher = StringMatcher.REGEX;
return this;
}
/**
* Sets string matcher to {@code stringMatcher}.
*
* @param stringMatcher must not be {@literal null}.
* @return
*/
public GenericPropertyMatcher stringMatcher(StringMatcher stringMatcher) {
Assert.notNull(stringMatcher, "StringMatcher must not be null!");
this.stringMatcher = stringMatcher;
return this;
}
/**
* Sets the {@link PropertyValueTransformer} to {@code propertyValueTransformer}.
*
* @param propertyValueTransformer must not be {@literal null}.
* @return
*/
public GenericPropertyMatcher transform(PropertyValueTransformer propertyValueTransformer) {
Assert.notNull(propertyValueTransformer, "PropertyValueTransformer must not be null!");
this.valueTransformer = propertyValueTransformer;
return this;
}
}
/**
* Predefined property matchers to create a {@link GenericPropertyMatcher}.
*
* @author Mark Paluch
*/
public static class GenericPropertyMatchers {
/**
* Creates a {@link GenericPropertyMatcher} that matches string case insensitive.
*
* @return
*/
public static GenericPropertyMatcher ignoreCase() {
return new GenericPropertyMatcher().ignoreCase();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string case sensitive.
*
* @return
*/
public static GenericPropertyMatcher caseSensitive() {
return new GenericPropertyMatcher().caseSensitive();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#CONTAINING}.
*
* @return
*/
public static GenericPropertyMatcher contains() {
return new GenericPropertyMatcher().contains();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#ENDING}.
*
* @return
*/
public static GenericPropertyMatcher endsWith() {
return new GenericPropertyMatcher().endsWith();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#STARTING}.
*
* @return
*/
public static GenericPropertyMatcher startsWith() {
return new GenericPropertyMatcher().startsWith();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#EXACT}.
*
* @return
*/
public static GenericPropertyMatcher exact() {
return new GenericPropertyMatcher().exact();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#DEFAULT}.
*
* @return
*/
public static GenericPropertyMatcher storeDefaultMatching() {
return new GenericPropertyMatcher().storeDefaultMatching();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#REGEX}.
*
* @return
*/
public static GenericPropertyMatcher regex() {
return new GenericPropertyMatcher().regex();
}
}
/**
* Match modes for treatment of {@link String} values.
*
* @author Christoph Strobl
*/
public static enum StringMatcher {
/**
* Store specific default.
*/
DEFAULT,
/**
* Matches the exact string
*/
EXACT,
/**
* Matches string starting with pattern
*/
STARTING,
/**
* Matches string ending with pattern
*/
ENDING,
/**
* Matches string containing pattern
*/
CONTAINING,
/**
* Treats strings as regular expression patterns
*/
REGEX;
}
/**
* Allows to transform the property value before it is used in the query.
*/
public static interface PropertyValueTransformer extends Converter<Object, Object> {}
/**
* @author Christoph Strobl
* @since 1.12
*/
public static enum NoOpPropertyValueTransformer implements ExampleMatcher.PropertyValueTransformer {
INSTANCE;
@Override
public Object convert(Object source) {
return source;
}
}
/**
* Define specific property handling for a Dot-Path.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.12
*/
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public static class PropertySpecifier {
String path;
StringMatcher stringMatcher;
Boolean ignoreCase;
PropertyValueTransformer valueTransformer;
/**
* Creates new {@link PropertySpecifier} for given path.
*
* @param path Dot-Path to the property. Must not be {@literal null}.
*/
PropertySpecifier(String path) {
Assert.hasText(path, "Path must not be null/empty!");
this.path = path;
this.stringMatcher = null;
this.ignoreCase = null;
this.valueTransformer = NoOpPropertyValueTransformer.INSTANCE;
}
/**
* Creates a new {@link PropertySpecifier} containing all values from the current instance and sets
* {@link StringMatcher} in the returned instance.
*
* @param stringMatcher must not be {@literal null}.
* @return
*/
public PropertySpecifier withStringMatcher(StringMatcher stringMatcher) {
Assert.notNull(stringMatcher, "StringMatcher must not be null!");
return new PropertySpecifier(this.path, stringMatcher, this.ignoreCase, this.valueTransformer);
}
/**
* Creates a new {@link PropertySpecifier} containing all values from the current instance and sets
* {@code ignoreCase}.
*
* @param ignoreCase must not be {@literal null}.
* @return
*/
public PropertySpecifier withIgnoreCase(boolean ignoreCase) {
return new PropertySpecifier(this.path, this.stringMatcher, ignoreCase, this.valueTransformer);
}
/**
* Creates a new {@link PropertySpecifier} containing all values from the current instance and sets
* {@link PropertyValueTransformer} in the returned instance.
*
* @param valueTransformer must not be {@literal null}.
* @return
*/
public PropertySpecifier withValueTransformer(PropertyValueTransformer valueTransformer) {
Assert.notNull(valueTransformer, "PropertyValueTransformer must not be null!");
return new PropertySpecifier(this.path, this.stringMatcher, this.ignoreCase, valueTransformer);
}
/**
* Get the properties Dot-Path.
*
* @return never {@literal null}.
*/
public String getPath() {
return path;
}
/**
* Get the {@link StringMatcher}.
*
* @return can be {@literal null}.
*/
public StringMatcher getStringMatcher() {
return stringMatcher;
}
/**
* @return {@literal null} if not set.
*/
public Boolean getIgnoreCase() {
return ignoreCase;
}
/**
* Get the property transformer to be applied.
*
* @return never {@literal null}.
*/
public PropertyValueTransformer getPropertyValueTransformer() {
return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer;
}
/**
* Transforms a given source using the {@link PropertyValueTransformer}.
*
* @param source
* @return
*/
public Object transformValue(Object source) {
return getPropertyValueTransformer().convert(source);
}
}
/**
* Define specific property handling for Dot-Paths.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.12
*/
@EqualsAndHashCode
public static class PropertySpecifiers {
private final Map<String, PropertySpecifier> propertySpecifiers = new LinkedHashMap<>();
PropertySpecifiers() {}
PropertySpecifiers(PropertySpecifiers propertySpecifiers) {
this.propertySpecifiers.putAll(propertySpecifiers.propertySpecifiers);
}
public void add(PropertySpecifier specifier) {
Assert.notNull(specifier, "PropertySpecifier must not be null!");
propertySpecifiers.put(specifier.getPath(), specifier);
}
public boolean hasSpecifierForPath(String path) {
return propertySpecifiers.containsKey(path);
}
public PropertySpecifier getForPath(String path) {
return propertySpecifiers.get(path);
}
public boolean hasValues() {
return !propertySpecifiers.isEmpty();
}
public Collection<PropertySpecifier> getSpecifiers() {
return propertySpecifiers.values();
}
}
/**
* The match modes to expose so that clients can find about how to concatenate the predicates.
*
* @author Oliver Gierke
* @since 1.13
* @see ExampleMatcher#isAllMatching()
*/
private static enum MatchMode {
ALL, ANY;
}
}