org.springframework.data.domain.ExampleMatcher Maven / Gradle / Ivy
/*
* 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 lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
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.
*
* This class is immutable.
*
* @author Christoph Strobl
* @author Mark Paluch
* @param
* @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 ignoredPaths;
boolean defaultIgnoreCase;
private ExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections. emptySet(), false);
}
/**
* Create a new untyped {@link ExampleMatcher} including all non-null properties by default.
*
* @param type must not be {@literal null}.
* @return
*/
public static ExampleMatcher matching() {
return new ExampleMatcher();
}
/**
* 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 newIgnoredPaths = new LinkedHashSet(this.ignoredPaths);
newIgnoredPaths.addAll(Arrays.asList(ignoredPaths));
return new ExampleMatcher(nullHandler, defaultStringMatcher, propertySpecifiers, newIgnoredPaths,
defaultIgnoreCase);
}
/**
* 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);
}
/**
* 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);
}
/**
* 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 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);
}
/**
* 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);
}
/**
* 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);
}
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 new ExampleMatcher(NullHandler.INCLUDE, defaultStringMatcher, propertySpecifiers, ignoredPaths,
defaultIgnoreCase);
}
/**
* 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 new ExampleMatcher(NullHandler.IGNORE, defaultStringMatcher, propertySpecifiers, ignoredPaths,
defaultIgnoreCase);
}
/**
* 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);
}
/**
* 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 getIgnoredPaths() {
return ignoredPaths;
}
/**
* @return the {@link PropertySpecifiers} within the {@link ExampleMatcher}.
*/
public PropertySpecifiers getPropertySpecifiers() {
return propertySpecifiers;
}
/**
* Null handling for creating criterion out of an {@link Example}.
*
* @author Christoph Strobl
*/
public static enum NullHandler {
INCLUDE, IGNORE
}
/**
* Callback to configure a matcher.
*
* @author Mark Paluch
* @param
*/
public static interface MatcherConfigurer {
void configureMatcher(T matcher);
}
/**
* A generic property matcher that specifies {@link StringMatcher string matching} and case sensitivity.
*
* @author Mark Paluch
*/
@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().startsWith();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#DEFAULT}.
*
* @return
*/
public static GenericPropertyMatcher storeDefaultMatching() {
return new GenericPropertyMatcher().storeDefaultMatching();
}
/**
* Creates a {@link GenericPropertyMatcher} that matches string using {@link StringMatcher#REGEX}.
*
* @return
*/
public static GenericPropertyMatcher regex() {
return new GenericPropertyMatcher().regex();
}
}
/**
* Match modes for treatment of {@link String} values.
*
* @author Christoph Strobl
*/
public static enum StringMatcher {
/**
* Store specific default.
*/
DEFAULT,
/**
* Matches the exact string
*/
EXACT,
/**
* Matches string starting with pattern
*/
STARTING,
/**
* Matches string ending with pattern
*/
ENDING,
/**
* Matches string containing pattern
*/
CONTAINING,
/**
* Treats strings as regular expression patterns
*/
REGEX;
}
/**
* Allows to transform the property value before it is used in the query.
*/
public static interface PropertyValueTransformer extends Converter