DATACMNS-669 - Final polishing.

Mostly Javadoc. Removed all API that took String paths so far.

Changed return type of bean definition for QuerydslPredicateArgumentResolver to HandlerMethodArgumentResolver so that Querydsl stays optional during class loading.

Original pull request: #132.
This commit is contained in:
Oliver Gierke
2015-07-17 19:41:45 +02:00
parent 64c8549db4
commit 0330cdc3d3
10 changed files with 340 additions and 170 deletions

View File

@@ -38,6 +38,8 @@ import org.springframework.format.support.FormattingConversionService;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.mysema.query.types.Predicate;
/**
* Configuration class to register {@link PageableHandlerMethodArgumentResolver},
* {@link SortHandlerMethodArgumentResolver} and {@link DomainClassConverter}.
@@ -70,13 +72,14 @@ public class SpringDataWebConfiguration extends WebMvcConfigurerAdapter {
}
/**
* Default QuerydslPredicateArgumentResolver.
* Default {@link QuerydslPredicateArgumentResolver} to create Querydsl {@link Predicate} instances for Spring MVC
* controller methods.
*
* @return
*/
@Bean
@Lazy
public QuerydslPredicateArgumentResolver querydslPredicateArgumentResolver() {
@Bean
public HandlerMethodArgumentResolver querydslPredicateArgumentResolver() {
return new QuerydslPredicateArgumentResolver(conversionService.getObject());
}

View File

@@ -31,7 +31,9 @@ import com.mysema.query.types.Predicate;
public interface MultiValueBinding<T extends Path<? extends S>, S> {
/**
* Returns the predicate to be applied to the given {@link Path} for the given value.
* Returns the predicate to be applied to the given {@link Path} for the given collection value, which will contain
* all values submitted for the path bind. If a single value was provided only the collection will consist of exactly
* one element.
*
* @param path {@link Path} to the property. Will not be {@literal null}.
* @param value the value that should be bound. Will not be {@literal null} or empty.

View File

@@ -80,18 +80,14 @@ public class QuerydslBindings {
return new PathBinder<T, S>(paths);
}
public final <T> TypeBinder<T> bind(Class<T> type) {
return new TypeBinder<T>(type);
}
/**
* Defines a binding for the given
* Returns a new {@link TypeBinder} for the given type.
*
* @param paths
* @param type must not be {@literal null}.
* @return
*/
final PropertyBinder bind(String... paths) {
return new PropertyBinder(Arrays.asList(paths));
public final <T> TypeBinder<T> bind(Class<T> type) {
return new TypeBinder<T>(type);
}
/**
@@ -109,16 +105,6 @@ public class QuerydslBindings {
}
}
/**
* Exclude properties from binding. Exclusion of all properties of a nested type can be done by exclusion on a higher
* level. E.g. {@code address} would exclude both {@code address.city} and {@code address.street}.
*
* @param properties
*/
final void excluding(String... properties) {
this.blackList.addAll(Arrays.asList(properties));
}
/**
* Include properties for binding. Include the property considered a binding candidate.
*
@@ -133,18 +119,6 @@ public class QuerydslBindings {
}
}
/**
* Include properties for binding. Include the property considered a binding candidate.
*
* @param properties
*/
final void including(String... properties) {
Assert.notEmpty(properties, "At least one property has to be provided!");
this.whiteList.addAll(Arrays.asList(properties));
}
/**
* Returns whether to exclude all properties for which no explicit binding has been defined or it has been explicitly
* white-listed. This defaults to {@literal false} which means that for properties without an explicitly defined
@@ -281,19 +255,25 @@ public class QuerydslBindings {
}
/**
* Defines the given {@link SingleValueBinding} to be used for the paths,
* Defines the given {@link SingleValueBinding} to be used for the paths.
*
* @param binding must not be {@literal null}.
* @return
*/
public void single(SingleValueBinding<P, T> binding) {
public void first(SingleValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
multi(new MultiValueBindingAdapter<P, T>(binding));
all(new MultiValueBindingAdapter<P, T>(binding));
}
public void multi(MultiValueBinding<P, T> binding) {
/**
* Defines the given {@link MultiValueBinding} to be used for the paths.
*
* @param binding must not be {@literal null}.
* @return
*/
public void all(MultiValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
@@ -303,54 +283,53 @@ public class QuerydslBindings {
}
}
/**
* A binder for types.
*
* @author Oliver Gierke
*/
public final class TypeBinder<T> {
private final Class<T> type;
public TypeBinder(Class<T> type) {
/**
* Creates a new {@link TypeBinder} for the given type.
*
* @param type must not be {@literal null}.
*/
private TypeBinder(Class<T> type) {
Assert.notNull(type, "Type must not be null!");
this.type = type;
}
public <P extends Path<T>> void single(SingleValueBinding<P, T> binding) {
/**
* Configures the given {@link SingleValueBinding} to be used for the current type.
*
* @param binding must not be {@literal null}.
*/
public <P extends Path<T>> void first(SingleValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
multi(new MultiValueBindingAdapter<P, T>(binding));
all(new MultiValueBindingAdapter<P, T>(binding));
}
public <P extends Path<T>> void multi(MultiValueBinding<P, T> binding) {
/**
* Configures the given {@link MultiValueBinding} to be used for the current type.
*
* @param binding must not be {@literal null}.
*/
public <P extends Path<T>> void all(MultiValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
QuerydslBindings.this.typeSpecs.put(type, new PathAndBinding<P, T>(null, binding));
}
}
public final class PropertyBinder {
private final List<String> paths;
private PropertyBinder(List<String> paths) {
this.paths = paths;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void using(SingleValueBinding<?, ?> binding) {
Assert.notNull(binding, "Binding must not be null!");
using(new MultiValueBindingAdapter(binding));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public void using(MultiValueBinding<?, ?> binding) {
Assert.notNull(binding, "Binding must not be null!");
for (String path : paths) {
QuerydslBindings.this.pathSpecs.put(path, new PathAndBinding(null, binding));
}
}
}
/**
* A pair of a {@link Path} and the registered {@link MultiValueBinding}.
*
* @author Christoph Strobl
* @since 1.11
*/
@@ -359,6 +338,12 @@ public class QuerydslBindings {
private final Path<?> path;
private final MultiValueBinding<S, T> binding;
/**
* Creates a new {@link PathAndBinding} for the given {@link Path} and {@link MultiValueBinding}.
*
* @param path must not be {@literal null}.
* @param binding must not be {@literal null}.
*/
public PathAndBinding(S path, MultiValueBinding<S, T> binding) {
this.path = path;
@@ -375,6 +360,9 @@ public class QuerydslBindings {
}
/**
* {@link MultiValueBinding} that forwards the first value of the collection values to the delegate
* {@link SingleValueBinding}.
*
* @author Oliver Gierke
*/
static class MultiValueBindingAdapter<T extends Path<? extends S>, S> implements MultiValueBinding<T, S> {
@@ -382,7 +370,9 @@ public class QuerydslBindings {
private final SingleValueBinding<T, S> delegate;
/**
* @param delegate
* Creates a new {@link MultiValueBindingAdapter} for the given {@link SingleValueBinding}.
*
* @param delegate must not be {@literal null}.
*/
public MultiValueBindingAdapter(SingleValueBinding<T, S> delegate) {
this.delegate = delegate;

View File

@@ -21,10 +21,11 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link java.lang.annotation.Annotation} to specify aspects of {@link com.mysema.query.types.Predicate} used in Spring
* MVC {@link org.springframework.web.servlet.mvc.Controller}.
* Annotation to customize the binding of HTTP request parameters to a Querydsl {@link com.mysema.query.types.Predicate}
* in Spring MVC handler methods.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.11
*/
@Target({ ElementType.PARAMETER, ElementType.TYPE })
@@ -32,14 +33,18 @@ import java.lang.annotation.Target;
public @interface QuerydslPredicate {
/**
* Root type to be used for {@link com.mysema.query.types.Path}
* The root type to create the {@link com.mysema.query.types.Predicate}. Specify this explictly if the type is not
* contained in the controller method's return type.
*
* @return
*/
Class<?>root() default Object.class;
/**
* Configuration class providing options on a per field base.
* To customize the way individual properties' values should be bound to the predicate a
* {@link QuerydslBinderCustomizer} can be specified here. We'll try to obtain a Spring bean of this type but fall
* back to a plain instantiation if no bean is found in the current
* {@link org.springframework.beans.factory.BeanFactory}.
*
* @return
*/

View File

@@ -16,6 +16,7 @@
package org.springframework.data.web.querydsl;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.beans.BeanUtils;
@@ -27,10 +28,12 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -51,10 +54,12 @@ import com.mysema.query.types.Predicate;
*/
public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentResolver, ApplicationContextAware {
private static final String INVALID_DOMAIN_TYPE = "Unable to find Querydsl root type for detected domain type %s! User @%s's root attribute to define the domain type manually!";
private final QuerydslPredicateBuilder predicateBuilder;
private final EntityPathResolver resolver;
private final Map<TypeInformation<?>, EntityPath<?>> entityPaths;
private AutowireCapableBeanFactory beanFactory;
private Repositories repositories;
/**
@@ -63,8 +68,11 @@ public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentR
* @param conversionService defaults to {@link DefaultConversionService} if {@literal null}.
*/
public QuerydslPredicateArgumentResolver(ConversionService conversionService) {
this.resolver = SimpleEntityPathResolver.INSTANCE;
this.entityPaths = new ConcurrentReferenceHashMap<TypeInformation<?>, EntityPath<?>>();
this.predicateBuilder = new QuerydslPredicateBuilder(
conversionService == null ? new DefaultConversionService() : conversionService);
conversionService == null ? new DefaultConversionService() : conversionService, resolver);
}
/*
@@ -114,53 +122,61 @@ public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentR
QuerydslPredicate annotation = parameter.getParameterAnnotation(QuerydslPredicate.class);
TypeInformation<?> domainType = extractTypeInfo(parameter).getActualType();
return predicateBuilder.getPredicate(parameters, createBindings(annotation, domainType.getType()), domainType);
return predicateBuilder.getPredicate(domainType, parameters, createBindings(annotation, domainType));
}
private TypeInformation<?> extractTypeInfo(MethodParameter parameter) {
QuerydslBindings createBindings(QuerydslPredicate annotation, TypeInformation<?> domainType) {
QuerydslPredicate annotation = parameter.getParameterAnnotation(QuerydslPredicate.class);
if (annotation == null || Object.class.equals(annotation.root())) {
return ClassTypeInformation.fromReturnTypeOf(parameter.getMethod());
}
return ClassTypeInformation.from(annotation.root());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public QuerydslBindings createBindings(QuerydslPredicate annotation, Class<?> domainType) {
EntityPath<?> path = SimpleEntityPathResolver.INSTANCE.createPath(domainType);
QuerydslBinderCustomizer customizer = findCustomizerForDomainType(annotation, domainType);
EntityPath<?> path = verifyEntityPathPresent(domainType);
QuerydslBindings bindings = new QuerydslBindings();
if (customizer != null) {
customizer.customize(bindings, path);
}
findCustomizerForDomainType(annotation, domainType.getType()).customize(bindings, path);
return bindings;
}
@SuppressWarnings("unchecked")
/**
* Obtains the {@link QuerydslBinderCustomizer} for the given domain type. Will inspect the given annotation for a
* dedicatedly configured one or consider the domain types's repository.
*
* @param annotation
* @param domainType
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private QuerydslBinderCustomizer<EntityPath<?>> findCustomizerForDomainType(QuerydslPredicate annotation,
Class<?> domainType) {
if (annotation == null || (annotation != null && annotation.bindings().equals(QuerydslBinderCustomizer.class))) {
if (repositories != null && repositories.hasRepositoryFor(domainType)) {
if (annotation != null) {
Object repository = this.repositories.getRepositoryFor(domainType);
if (repository instanceof QuerydslBinderCustomizer) {
return (QuerydslBinderCustomizer<EntityPath<?>>) repository;
}
Class<? extends QuerydslBinderCustomizer> bindings = annotation.bindings();
if (bindings != QuerydslBinderCustomizer.class) {
return createQuerydslBinderCustomizer(bindings);
}
return null;
}
return createQuerydslBinderCustomizer(annotation.bindings());
if (repositories != null && repositories.hasRepositoryFor(domainType)) {
Object repository = repositories.getRepositoryFor(domainType);
if (repository instanceof QuerydslBinderCustomizer) {
return (QuerydslBinderCustomizer<EntityPath<?>>) repository;
}
}
return NoOpCustomizer.INSTANCE;
}
/**
* Obtains a {@link QuerydslBinderCustomizer} for the given type. Will try to obtain a bean from the
* {@link org.springframework.beans.factory.BeanFactory} first or fall back to create a fresh instance through the
* {@link org.springframework.beans.factory.BeanFactory} or finally falling back to a plain instantiation if no
* {@link org.springframework.beans.factory.BeanFactory} is present.
*
* @param type must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private QuerydslBinderCustomizer<EntityPath<?>> createQuerydslBinderCustomizer(
Class<? extends QuerydslBinderCustomizer> type) {
@@ -172,9 +188,80 @@ public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentR
try {
return beanFactory.getBean(type);
} catch (NoSuchBeanDefinitionException e) {
return beanFactory.createBean(type);
}
}
/**
* Tries to detect a Querydsl query type for the given domain type candidate via the configured
* {@link EntityPathResolver}.
*
* @param candidate must not be {@literal null}.
* @throws IllegalStateException to indicate the query type can't be found and manual configuration is necessary.
*/
private EntityPath<?> verifyEntityPathPresent(TypeInformation<?> candidate) {
EntityPath<?> path = entityPaths.get(candidate);
if (path != null) {
return path;
}
return beanFactory.createBean(type);
Class<?> type = candidate.getType();
try {
path = resolver.createPath(type);
} catch (IllegalArgumentException o_O) {
throw new IllegalStateException(
String.format(INVALID_DOMAIN_TYPE, candidate.getType(), QuerydslPredicate.class.getSimpleName()), o_O);
}
entityPaths.put(candidate, path);
return path;
}
/**
* Obtains the domain type information from the given method parameter. Will favor an explicitly registered on through
* {@link QuerydslPredicate#root()} but use the actual type of the method's return type as fallback.
*
* @param parameter must not be {@literal null}.
* @return
*/
static TypeInformation<?> extractTypeInfo(MethodParameter parameter) {
QuerydslPredicate annotation = parameter.getParameterAnnotation(QuerydslPredicate.class);
if (annotation != null && !Object.class.equals(annotation.root())) {
return ClassTypeInformation.from(annotation.root());
}
return detectDomainType(ClassTypeInformation.fromReturnTypeOf(parameter.getMethod()));
}
private static TypeInformation<?> detectDomainType(TypeInformation<?> source) {
if (source.getTypeArguments().isEmpty()) {
return source;
}
TypeInformation<?> actualType = source.getActualType();
if (source != actualType) {
return detectDomainType(actualType);
}
if (source instanceof Iterable) {
return source;
}
return detectDomainType(source.getComponentType());
}
private static enum NoOpCustomizer implements QuerydslBinderCustomizer<EntityPath<?>> {
INSTANCE;
@Override
public void customize(QuerydslBindings bindings, EntityPath<?> root) {}
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.beans.PropertyValues;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
@@ -51,24 +51,29 @@ public class QuerydslPredicateBuilder {
private final ConversionService conversionService;
private final MultiValueBinding<?, ?> defaultBinding;
private final Map<PropertyPath, Path<?>> paths;
private final EntityPathResolver resolver;
public QuerydslPredicateBuilder(ConversionService conversionService) {
public QuerydslPredicateBuilder(ConversionService conversionService, EntityPathResolver resolver) {
Assert.notNull(conversionService, "ConversionService must not be null!");
this.defaultBinding = new QuerydslDefaultBinding();
this.conversionService = conversionService;
this.paths = new HashMap<PropertyPath, Path<?>>();
this.resolver = resolver;
}
/**
* @param values
* @param context
* Creates a Querydsl {@link Predicate} for the given values, {@link QuerydslBindings} on the given
* {@link TypeInformation}.
*
* @param type the type to create a predicate for.
* @param values the values to bind.
* @param bindings the {@link QuerydslBindings} for the predicate.
* @return
*/
public Predicate getPredicate(MultiValueMap<String, String> values, QuerydslBindings bindings,
TypeInformation<?> type) {
public Predicate getPredicate(TypeInformation<?> type, MultiValueMap<String, String> values,
QuerydslBindings bindings) {
Assert.notNull(bindings, "Context must not be null!");
@@ -92,10 +97,10 @@ public class QuerydslPredicateBuilder {
Collection<Object> value = convertToPropertyPathSpecificType(entry.getValue(), propertyPath);
Predicate binding = invokeBinding(propertyPath, bindings, value);
Predicate predicate = invokeBinding(propertyPath, bindings, value);
if (binding != null) {
builder.and(binding);
if (predicate != null) {
builder.and(predicate);
}
}
} catch (PropertyReferenceException o_O) {
@@ -106,17 +111,34 @@ public class QuerydslPredicateBuilder {
return builder.getValue();
}
/**
* Invokes the binding of the given values, for the given {@link PropertyPath} and {@link QuerydslBindings}.
*
* @param dotPath must not be {@literal null}.
* @param bindings must not be {@literal null}.
* @param values must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate invokeBinding(PropertyPath dotPath, QuerydslBindings bindings, Collection<Object> value) {
private Predicate invokeBinding(PropertyPath dotPath, QuerydslBindings bindings, Collection<Object> values) {
Path<?> path = getPath(dotPath, bindings);
MultiValueBinding binding = bindings.getBindingForPath(dotPath);
binding = binding == null ? defaultBinding : binding;
return binding.bind(path, value);
return binding.bind(path, values);
}
/**
* Returns the {@link Path} for the given {@link PropertyPath} and {@link QuerydslBindings}. Will try to obtain the
* {@link Path} from the bindings first but fall back to reifying it from the PropertyPath in case no specific binding
* has been configured.
*
* @param path must not be {@literal null}.
* @param bindings must not be {@literal null}.
* @return
*/
private Path<?> getPath(PropertyPath path, QuerydslBindings bindings) {
Path<?> resolvedPath = bindings.getExistingPath(path);
@@ -137,10 +159,16 @@ public class QuerydslPredicateBuilder {
return resolvedPath;
}
private static Path<?> reifyPath(PropertyPath path, Path<?> base) {
/**
* Tries to reify a Querydsl {@link Path} from the given {@link PropertyPath} and base.
*
* @param path must not be {@literal null}.
* @param base can be {@literal null}.
* @return
*/
private Path<?> reifyPath(PropertyPath path, Path<?> base) {
Path<?> entityPath = base != null ? base
: SimpleEntityPathResolver.INSTANCE.createPath(path.getOwningType().getType());
Path<?> entityPath = base != null ? base : resolver.createPath(path.getOwningType().getType());
Field field = ReflectionUtils.findField(entityPath.getClass(), path.getSegment());
Object value = ReflectionUtils.getField(field, entityPath);
@@ -152,28 +180,34 @@ public class QuerydslPredicateBuilder {
return (Path<?>) value;
}
/**
* Converts the given source values into a collection of elements that are of the given {@link PropertyPath}'s type.
* Considers a single element list with an empty {@link String} an empty collection because this basically indicates
* the property having been submitted but no value provided.
*
* @param source must not be {@literal null}.
* @param path must not be {@literal null}.
* @return
*/
private Collection<Object> convertToPropertyPathSpecificType(List<String> source, PropertyPath path) {
Class<?> targetType = path.getLeafProperty().getType();
if (isSingleElementCollectionWithoutText(source)) {
if (source.isEmpty() || isSingleElementCollectionWithoutText(source)) {
return Collections.emptyList();
}
Collection<Object> target = new ArrayList<Object>(source.size());
for (String value : source) {
target.add(potentiallyConvertValue(value, targetType));
target.add(conversionService.canConvert(value.getClass(), targetType)
? conversionService.convert(value, targetType) : value);
}
return target;
}
private Object potentiallyConvertValue(Object source, Class<?> targetType) {
return conversionService.canConvert(source.getClass(), targetType) ? conversionService.convert(source, targetType)
: source;
}
/**
* Returns whether the given collection has exactly one element that doesn't contain any text. This is basically an
* indicator that a request parameter has been submitted but no value for it.

View File

@@ -30,7 +30,8 @@ import com.mysema.query.types.Predicate;
public interface SingleValueBinding<T extends Path<? extends S>, S> {
/**
* Returns the predicate to be applied to the given {@link Path} for the given value.
* Returns the predicate to be applied to the given {@link Path} for the given value. The given value will be the first
* the first one provided for the given path and converted into the expected type.
*
* @param path {@link Path} to the property. Will not be {@literal null}.
* @param value the value that should be bound. Will not be {@literal null}.