From 5db3def79b066caa41d3b0f61e9d51936c37736b Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 15 Jul 2015 09:24:52 +0200 Subject: [PATCH] DATACMNS-669 - Polishing. QuerydslBindings now uses a builder style pattern to define custom bindings. This allows to define the same binding for multiple properties of the same type in one configuration call. Introduced MultiValueBinding to be able to register a binding for multiple source values (i.e. if multiple values are provided for the same parameter). The previously named QuerydslBinding is now a SingleValueBinding which gets adapted to by extracting the first element of the values provided considered as collection. Slightly refined the generics signature in QuerydslBinding for better type derivation in Lambdas. Removed the need for QuerydslBindingContext by moving a few properties around. The ConversionService instance is now piped into the QuerydslPredicateBuilder directly. Added a bit of sample data to the Users test domain type to be able to execute the generated Predicates in tests to validate their correctness. Original pull request: #132. --- ...dslBinding.java => MultiValueBinding.java} | 17 +- .../web/querydsl/QuerydslBindingContext.java | 124 ------- .../data/web/querydsl/QuerydslBindings.java | 339 +++++++++++++----- .../web/querydsl/QuerydslDefaultBinding.java | 25 +- .../QuerydslPredicateArgumentResolver.java | 53 +-- .../querydsl/QuerydslPredicateBuilder.java | 166 +++++---- .../data/web/querydsl/SingleValueBinding.java | 40 +++ .../data/querydsl/Address.java | 9 +- .../springframework/data/querydsl/User.java | 17 +- .../springframework/data/querydsl/Users.java | 37 ++ .../querydsl/QuerydslBindingsUnitTests.java | 192 +++++----- .../QuerydslDefaultBindingUnitTests.java | 9 +- ...dslPredicateArgumentResolverUnitTests.java | 51 +-- .../QuerydslPredicateBuilderUnitTests.java | 60 +++- 14 files changed, 652 insertions(+), 487 deletions(-) rename src/main/java/org/springframework/data/web/querydsl/{QuerydslBinding.java => MultiValueBinding.java} (64%) delete mode 100644 src/main/java/org/springframework/data/web/querydsl/QuerydslBindingContext.java create mode 100644 src/main/java/org/springframework/data/web/querydsl/SingleValueBinding.java create mode 100644 src/test/java/org/springframework/data/querydsl/Users.java diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslBinding.java b/src/main/java/org/springframework/data/web/querydsl/MultiValueBinding.java similarity index 64% rename from src/main/java/org/springframework/data/web/querydsl/QuerydslBinding.java rename to src/main/java/org/springframework/data/web/querydsl/MultiValueBinding.java index 1b324bb59..681e0a0c3 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslBinding.java +++ b/src/main/java/org/springframework/data/web/querydsl/MultiValueBinding.java @@ -15,22 +15,15 @@ */ package org.springframework.data.web.querydsl; +import java.util.Collection; + import com.mysema.query.types.Path; import com.mysema.query.types.Predicate; /** - * {@link QuerydslBinding} creates a {@link Predicate} out of given {@link Path} and value. Used for specific parameter - * treatment in {@link QuerydslBindings}.
- * - * @author Christoph Strobl - * @since 1.11 + * @author Oliver Gierke */ -public interface QuerydslBinding> { +public interface MultiValueBinding, S> { - /** - * @param path {@link Path} to the property. Must not be {@literal null}. - * @param value - * @return - */ - Predicate bind(T path, Object value); + Predicate bind(T path, Collection value); } diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslBindingContext.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslBindingContext.java deleted file mode 100644 index 5a182377d..000000000 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslBindingContext.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2015 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.web.querydsl; - -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.util.TypeInformation; -import org.springframework.util.Assert; - -import com.mysema.query.types.Path; - -/** - * {@link QuerydslBindingContext} holds parameters required for executing {@link QuerydslBinding} on a specific property - * and {@link Path}. - * - * @author Christoph Strobl - * @since 1.11 - */ -class QuerydslBindingContext { - - private final TypeInformation rootTypeInformation; - private final ConversionService conversionService; - private final QuerydslBindings bindings; - - /** - * @param rootTypeInformation Root type information. Must not be {@literal null}. - * @param bindings Path specific bindings. Defaulted to {@link QuerydslBindings} if {@literal null}. - * @param conversionService {@link ConversionService} to be used for converting request parameters into target type. - * Defaulted to {@link DefaultConversionService} if {@literal null}. - */ - public QuerydslBindingContext(TypeInformation rootTypeInformation, QuerydslBindings bindings, - ConversionService conversionService) { - - Assert.notNull(rootTypeInformation, "TypeInformation must not be null!"); - - this.conversionService = conversionService == null ? new DefaultConversionService() : conversionService; - this.rootTypeInformation = rootTypeInformation; - this.bindings = bindings == null ? new QuerydslBindings() : bindings; - } - - /** - * {@link ConversionService} to be used for converting parameter values. - * - * @return never {@literal null}. - */ - public ConversionService getConversionService() { - return this.conversionService; - } - - /** - * Root type information - * - * @return never {@literal null}. - */ - public TypeInformation getTypeInformation() { - return rootTypeInformation; - } - - /** - * {@link QuerydslBindings} to be considered. - * - * @return never {@literal null}. - */ - public QuerydslBindings getBindings() { - return bindings; - } - - /** - * Specific {@link QuerydslBinding} for {@link PropertyPath}. - * - * @param path must not be {@literal null}. - * @return {@literal null} when no specific {@link QuerydslBinding} set for path. - * @see QuerydslBindings#getBindingForPath(PropertyPath) - */ - public QuerydslBinding> getBindingForPath(PropertyPath path) { - return bindings.getBindingForPath(path); - } - - /** - * Specific {@link Path} for {@link PropertyPath}. - * - * @param path - * @return - * @see QuerydslBindings#getPath(PropertyPath) - */ - public Path getPathForPropertyPath(PropertyPath path) { - return bindings.getPath(path); - } - - /** - * Checks visibility of given path against {@link QuerydslBindings}. - * - * @param path must not be {@literal null}. - * @return true when given path is visible by declaration. - * @see QuerydslBindings#isPathVisible(PropertyPath) - */ - public boolean isPathVisible(PropertyPath path) { - return bindings.isPathVisible(path); - } - - /** - * Get the actual raw type of the used root. - * - * @return - * @see TypeInformation#getActualType() - */ - public Class getTargetType() { - return rootTypeInformation.getActualType().getType(); - } -} diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslBindings.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslBindings.java index f0bbb3c71..c4ab1b510 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslBindings.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslBindings.java @@ -16,6 +16,7 @@ package org.springframework.data.web.querydsl; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -27,92 +28,135 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.mysema.query.types.Path; +import com.mysema.query.types.PathMetadata; +import com.mysema.query.types.Predicate; /** - * {@link QuerydslBindings} allows definition of path specific {@link QuerydslBinding}. * + * {@link QuerydslBindings} allows definition of path specific {@link SingleValueBinding}. * *
  * 
  * new QuerydslBindings() {
  *   {
- *     bind(QUser.user.address.city, (path, value) -> path.like(value.toString()));
- *     bind(new StringPath("address.city"), (path, value) -> path.like(value.toString()));
+ *     bind(QUser.user.address.city).using((path, value) -> path.like(value.toString()));
+ *     bind("lastname").using((path, value) -> path.like(value.toString()));
  *   }
  * }
  * 
  * 
* * @author Christoph Strobl + * @author Oliver Gierke * @since 1.11 */ public class QuerydslBindings { - private final Map pathSpecs; - private final Map, PathAndBinding> typeSpecs; - private Set whiteList; + private final Map> pathSpecs; + private final Map, PathAndBinding> typeSpecs; + private final Set whiteList; private final Set blackList; + private boolean excludeUnlistedProperties; + + /** + * Creates a new {@link QuerydslBindings} instance. + */ public QuerydslBindings() { - this.pathSpecs = new LinkedHashMap(); - this.typeSpecs = new LinkedHashMap, PathAndBinding>(); + this.pathSpecs = new LinkedHashMap>(); + this.typeSpecs = new LinkedHashMap, PathAndBinding>(); this.whiteList = new HashSet(); this.blackList = new HashSet(); } /** - * @param path - * @param builder + * Returns a new {@link PathBinder} for the given {@link Path}s to define bindings for them. + * + * @param paths must not be {@literal null} or emtpy. + * @return */ - protected QuerydslBindings bind(String path, QuerydslBinding> builder) { - - Assert.hasText(path, "Cannot bind to null/empty path"); - this.pathSpecs.put(path, new PathAndBinding(null, builder)); - return this; + public final , S> PathBinder bind(T... paths) { + return new PathBinder(paths); } /** - * @param type - * @param builder + * Defines a binding for the given + * + * @param paths + * @return */ - protected QuerydslBindings bind(Class type, QuerydslBinding> builder) { + public final PropertyBinder bind(String... paths) { + return new PropertyBinder(Arrays.asList(paths)); + } - Assert.notNull(type, "Cannot bind to null type!"); - this.typeSpecs.put(type, new PathAndBinding(null, builder)); - return this; + public final TypeBinder bind(Class type) { + return new TypeBinder(type); } /** - * @param path - * @param builder + * 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 paths must not be {@literal null} or empty. */ - protected > QuerydslBindings bind(T path, QuerydslBinding builder) { + public final void excluding(Path... paths) { - Assert.notNull(path, "Cannot bind to null path!"); - this.pathSpecs.put(extractPropertyPath(path), new PathAndBinding(path, builder)); - return this; + Assert.notEmpty(paths, "At least one path has to be provided!"); + + for (Path path : paths) { + this.blackList.add(toDotPath(path)); + } } /** - * Exclude properties from binding.
- * Exclusion of all properties of a nested type can be done by exclusion on a higher level. Eg. {@code address} would - * exclude both {@code address.city} and {@code address.street}. + * 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 */ - protected QuerydslBindings excluding(String... properties) { + public final void excluding(String... properties) { this.blackList.addAll(Arrays.asList(properties)); - return this; } /** - * Include properties for binding.
- * Include the property considered a binding candidate. + * Include properties for binding. Include the property considered a binding candidate. + * + * @param properties must not be {@literal null} or empty. + */ + public final void including(Path... paths) { + + Assert.notEmpty(paths, "At least one path has to be provided!"); + + for (Path path : paths) { + this.whiteList.add(toDotPath(path)); + } + } + + /** + * Include properties for binding. Include the property considered a binding candidate. * * @param properties */ - protected QuerydslBindings including(String... properties) { + public 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 + * binding a type specific default binding will be applied. + * + * @param excludeUnlistedProperties + * @return + * @see #including(String...) + * @see #including(Path...) + */ + public final QuerydslBindings excludeUnlistedProperties(boolean excludeUnlistedProperties) { + + this.excludeUnlistedProperties = excludeUnlistedProperties; return this; } @@ -122,7 +166,7 @@ public class QuerydslBindings { * @param path * @return */ - public boolean isPathVisible(PropertyPath path) { + boolean isPathVisible(PropertyPath path) { List segments = Arrays.asList(path.toDotPath().split("\\.")); @@ -134,6 +178,7 @@ public class QuerydslBindings { if (!whiteList.isEmpty()) { return whiteList.contains(path.toDotPath()); } + return false; } } @@ -141,96 +186,179 @@ public class QuerydslBindings { return true; } + /** + * Returns the {@link SingleValueBinding} for the given {@link PropertyPath}. Prefers a path configured for the + * specific path but falls back to the builder registered for a given type. + * + * @param path must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + , T> MultiValueBinding getBindingForPath(PropertyPath path) { + + Assert.notNull(path, "PropertyPath must not be null!"); + + PathAndBinding pathAndBinding = (PathAndBinding) pathSpecs.get(path.toDotPath()); + + if (pathAndBinding != null) { + return pathAndBinding.getBinding(); + } + + pathAndBinding = (PathAndBinding) typeSpecs.get(path.getLeafProperty().getType()); + + return pathAndBinding == null ? null : pathAndBinding.getBinding(); + } + + /** + * Returns a {@link Path} for the {@link PropertyPath} instance. + * + * @param path must not be {@literal null}. + * @return + */ + Path getExistingPath(PropertyPath path) { + + PathAndBinding pathAndBuilder = pathSpecs.get(path.toDotPath()); + return pathAndBuilder == null ? null : pathAndBuilder.getPath(); + } + + /** + * Returns whether the given path is visible, which means either on the white list or not on the black list if no + * white list configured. + * + * @param path must not be {@literal null}. + * @return + */ private boolean isPathVisible(String path) { - if (whiteList.isEmpty() && blackList.isEmpty()) { - return true; - } - - if (!blackList.isEmpty()) { - - if (blackList.contains(path)) { - - if (!whiteList.isEmpty() && whiteList.contains(path)) { - return true; - } - return false; - } - - return true; - } - if (!whiteList.isEmpty()) { if (whiteList.contains(path)) { return true; } + return false; } - return true; + return excludeUnlistedProperties ? false : !blackList.contains(path); } /** - * Returns the {@link QuerydslBinding} for the given {@link PropertyPath}. Prefers a path configured for the specific - * path but falls back to the builder registered for a given type. + * Returns the property path for the given {@link Path}. * - * @param path must not be {@literal null}. + * @param path can be {@literal null}. * @return */ - public QuerydslBinding> getBindingForPath(PropertyPath path) { - - Assert.notNull(path, "PropertyPath must not be null!"); - - PathAndBinding pathAndBinding = pathSpecs.get(path.toDotPath()); - - if (pathAndBinding != null) { - return pathAndBinding.getBinding(); - } - - pathAndBinding = typeSpecs.get(path.getLeafProperty().getType()); - - return pathAndBinding == null ? null : pathAndBinding.getBinding(); - } - - Path getPath(PropertyPath path) { - return getPathForStringPath(path.toDotPath()); - } - - private Path getPathForStringPath(String path) { - - PathAndBinding pathAndBuilder = pathSpecs.get(path); - if (pathAndBuilder == null) { - return null; - } - - return pathAndBuilder.getPath(); - } - - private String extractPropertyPath(Path path) { + private String toDotPath(Path path) { if (path == null) { return ""; } - if (path.getMetadata().getParent() != null && !path.getMetadata().getParent().getMetadata().isRoot()) { - return extractPropertyPath(path.getMetadata().getParent()) + "." + path.getMetadata().getName(); + PathMetadata metadata = path.getMetadata(); + + return path.toString().substring(metadata.getRoot().getMetadata().getName().length() + 1); + } + + /** + * A binder for {@link Path}s. + * + * @author Oliver Gierke + */ + protected final class PathBinder

, T> { + + private final List

paths; + + /** + * Creates a new {@link PathBinder} for the given {@link Path}s. + * + * @param paths must not be {@literal null} or empty. + */ + public PathBinder(P... paths) { + + Assert.notEmpty(paths, "At least one path has to be provided!"); + this.paths = Arrays.asList(paths); } - return path.getMetadata().getName(); + /** + * Defines the given {@link SingleValueBinding} to be used for the paths, + * + * @param binding must not be {@literal null}. + * @return + */ + public void single(SingleValueBinding binding) { + Assert.notNull(binding, "Binding must not be null!"); + + multi(new MultiValueBindingAdapter(binding)); + } + + public void multi(MultiValueBinding binding) { + + Assert.notNull(binding, "Binding must not be null!"); + + for (P path : paths) { + QuerydslBindings.this.pathSpecs.put(toDotPath(path), new PathAndBinding(path, binding)); + } + } + } + + protected final class TypeBinder { + + private final Class type; + + public TypeBinder(Class type) { + this.type = type; + } + + public

> void single(SingleValueBinding binding) { + + Assert.notNull(binding, "Binding must not be null!"); + multi(new MultiValueBindingAdapter(binding)); + } + + public

> void multi(MultiValueBinding binding) { + + Assert.notNull(binding, "Binding must not be null!"); + QuerydslBindings.this.typeSpecs.put(type, new PathAndBinding(null, binding)); + } + } + + protected final class PropertyBinder { + + private final List paths; + + private PropertyBinder(List 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)); + } + } } /** * @author Christoph Strobl * @since 1.11 */ - private static class PathAndBinding { + private static class PathAndBinding, T> { private final Path path; - private final QuerydslBinding> binding; + private final MultiValueBinding binding; - public PathAndBinding(Path path, QuerydslBinding> binding) { + public PathAndBinding(S path, MultiValueBinding binding) { this.path = path; this.binding = binding; @@ -240,9 +368,32 @@ public class QuerydslBindings { return path; } - public QuerydslBinding> getBinding() { + public MultiValueBinding getBinding() { return binding; } + } + /** + * @author Oliver Gierke + */ + static class MultiValueBindingAdapter, S> implements MultiValueBinding { + + private final SingleValueBinding delegate; + + /** + * @param delegate + */ + public MultiValueBindingAdapter(SingleValueBinding delegate) { + this.delegate = delegate; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.web.querydsl.MultiValueBinding#bind(com.mysema.query.types.Path, java.util.Collection) + */ + @Override + public Predicate bind(T path, Collection value) { + return delegate.bind(path, value.iterator().next()); + } } } diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslDefaultBinding.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslDefaultBinding.java index 631f7a219..31e05f2d0 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslDefaultBinding.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslDefaultBinding.java @@ -23,7 +23,8 @@ import com.mysema.query.types.expr.SimpleExpression; import com.mysema.query.types.path.CollectionPathBase; /** - * Default implementation of {@link QuerydslBinding} creating {@link Predicate} based on the {@link Path}s type. Binds: + * Default implementation of {@link MultiValueBinding} creating {@link Predicate} based on the {@link Path}s type. + * Binds: *

    *
  • {@literal null} as {@link SimpleExpression#isNull()}.
  • *
  • {@link java.lang.Object} as {@link SimpleExpression#eq()} on simple properties.
  • @@ -32,9 +33,10 @@ import com.mysema.query.types.path.CollectionPathBase; *
* * @author Christoph Strobl + * @author Oliver Gierke * @since 1.11 */ -class QuerydslDefaultBinding implements QuerydslBinding> { +class QuerydslDefaultBinding implements MultiValueBinding, Object> { /* * (non-Javadoc) @@ -42,27 +44,28 @@ class QuerydslDefaultBinding implements QuerydslBinding> { */ @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Predicate bind(Path path, Object source) { + public Predicate bind(Path path, Collection source) { - if (source == null && path instanceof SimpleExpression) { + if ((source == null || source.isEmpty())) { return ((SimpleExpression) path).isNull(); } + Object firstValue = source.iterator().next(); + if (path instanceof CollectionPathBase) { - return ((CollectionPathBase) path).contains(source); + return ((CollectionPathBase) path).contains(firstValue); } if (path instanceof SimpleExpression) { - if (source instanceof Collection) { - return ((SimpleExpression) path).in((Collection) source); + if (source.size() > 1) { + return ((SimpleExpression) path).in(source); } - return ((SimpleExpression) path).eq(source); + return ((SimpleExpression) path).eq(firstValue); } - throw new IllegalArgumentException(String.format("Cannot create predicate for path '%s' with type '%s'.", path, - path.getMetadata().getPathType())); + throw new IllegalArgumentException( + String.format("Cannot create predicate for path '%s' with type '%s'.", path, path.getMetadata().getPathType())); } - } diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java index ac28b0b3f..caa2ba55c 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java @@ -16,7 +16,11 @@ package org.springframework.data.web.querydsl; import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -34,20 +38,32 @@ import com.mysema.query.types.Predicate; * controller methods. * * @author Christoph Strobl + * @author Oliver Gierke * @since 1.11 */ -public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentResolver { +public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentResolver, ApplicationContextAware { private final QuerydslPredicateBuilder predicateBuilder; - private final ConversionService conversionService; + + private AutowireCapableBeanFactory beanFactory; /** - * @param conversionService Defaulted to {@link DefaultConversionService} if {@literal null}. + * Creates a new {@link QuerydslPredicateArgumentResolver} using the given {@link ConversionService}. + * + * @param conversionService defaults to {@link DefaultConversionService} if {@literal null}. */ public QuerydslPredicateArgumentResolver(ConversionService conversionService) { + this.predicateBuilder = new QuerydslPredicateBuilder( + conversionService == null ? new DefaultConversionService() : conversionService); + } - this.conversionService = conversionService == null ? new DefaultConversionService() : conversionService; - this.predicateBuilder = new QuerydslPredicateBuilder(); + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.beanFactory = applicationContext.getAutowireCapableBeanFactory(); } /* @@ -62,9 +78,8 @@ public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentR } if (parameter.hasParameterAnnotation(QuerydslPredicate.class)) { - throw new IllegalArgumentException(String.format( - "Parameter at position %s must be of type Predicate but was %s.", parameter.getParameterIndex(), - parameter.getParameterType())); + throw new IllegalArgumentException(String.format("Parameter at position %s must be of type Predicate but was %s.", + parameter.getParameterIndex(), parameter.getParameterType())); } return false; @@ -75,29 +90,27 @@ public class QuerydslPredicateArgumentResolver implements HandlerMethodArgumentR * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory) */ @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + public Predicate resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { return predicateBuilder.getPredicate(new MutablePropertyValues(webRequest.getParameterMap()), - createBindingContext(parameter)); - } - - private QuerydslBindingContext createBindingContext(MethodParameter parameter) throws InstantiationException, - IllegalAccessException { - return new QuerydslBindingContext(extractTypeInfo(parameter), extractBindings(parameter), conversionService); + createBindings(parameter), extractTypeInfo(parameter).getActualType()); } private TypeInformation extractTypeInfo(MethodParameter parameter) { Class type = parameter.getParameterAnnotation(QuerydslPredicate.class).root(); - return type == Object.class ? ClassTypeInformation.fromReturnTypeOf(parameter.getMethod()) : ClassTypeInformation - .from(type); + return type == Object.class ? ClassTypeInformation.fromReturnTypeOf(parameter.getMethod()) + : ClassTypeInformation.from(type); } - private QuerydslBindings extractBindings(MethodParameter parameter) throws InstantiationException, - IllegalAccessException { + private QuerydslBindings createBindings(MethodParameter parameter) + throws InstantiationException, IllegalAccessException { - return BeanUtils.instantiateClass(parameter.getParameterAnnotation(QuerydslPredicate.class).bindings()); + Class bindingsType = parameter.getParameterAnnotation(QuerydslPredicate.class) + .bindings(); + + return beanFactory != null ? beanFactory.createBean(bindingsType) : BeanUtils.instantiateClass(bindingsType); } } diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilder.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilder.java index 93507738f..a8dd4c226 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilder.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilder.java @@ -15,42 +15,49 @@ */ package org.springframework.data.web.querydsl; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; -import java.util.List; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; +import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import com.mysema.query.BooleanBuilder; +import com.mysema.query.types.EntityPath; import com.mysema.query.types.Path; -import com.mysema.query.types.PathMetadata; -import com.mysema.query.types.PathMetadataFactory; import com.mysema.query.types.Predicate; -import com.mysema.query.types.path.CollectionPath; -import com.mysema.query.types.path.NumberPath; -import com.mysema.query.types.path.PathBuilder; -import com.mysema.query.types.path.PathBuilderFactory; -import com.mysema.query.types.path.SimplePath; -import com.mysema.query.types.path.StringPath; /** * Builder assembling {@link Predicate} out of {@link PropertyValues}. * * @author Christoph Strobl + * @author Oliver Gierke * @since 1.11 */ class QuerydslPredicateBuilder { - private final QuerydslBinding defaultBinding; + private final ConversionService conversionService; + private final MultiValueBinding defaultBinding; + private final Map> paths; + + public QuerydslPredicateBuilder(ConversionService conversionService) { + + Assert.notNull(conversionService, "ConversionService must not be null!"); - public QuerydslPredicateBuilder() { this.defaultBinding = new QuerydslDefaultBinding(); + this.conversionService = conversionService; + this.paths = new HashMap>(); } /** @@ -58,10 +65,10 @@ class QuerydslPredicateBuilder { * @param context * @return */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Predicate getPredicate(PropertyValues values, QuerydslBindingContext context) { - Assert.notNull(context, "Context must not be null!"); + public Predicate getPredicate(PropertyValues values, QuerydslBindings bindings, TypeInformation type) { + + Assert.notNull(bindings, "Context must not be null!"); if (values.isEmpty()) { return new BooleanBuilder(); @@ -71,109 +78,98 @@ class QuerydslPredicateBuilder { for (PropertyValue propertyValue : values.getPropertyValues()) { - PropertyPath propertyPath = PropertyPath.from(propertyValue.getName(), context.getTargetType()); + PropertyPath propertyPath = PropertyPath.from(propertyValue.getName(), type); - if (context.isPathVisible(propertyPath)) { + if (bindings.isPathVisible(propertyPath)) { - Object value = convertToPropertyPathSpecificType(propertyValue.getValue(), propertyPath, context); - QuerydslBinding binding = getPredicateBuilderForPath(propertyPath, context); - - builder.and(binding.bind(getPath(propertyPath, context), value)); + Collection value = convertToPropertyPathSpecificType(propertyValue.getValue(), propertyPath); + builder.and(invokeBinding(propertyPath, bindings, value)); } } return builder.getValue(); } - private QuerydslBinding> getPredicateBuilderForPath(PropertyPath dotPath, - QuerydslBindingContext context) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Predicate invokeBinding(PropertyPath dotPath, QuerydslBindings bindings, Collection value) { - QuerydslBinding> binding = context.getBindingForPath(dotPath); - return binding == null ? defaultBinding : binding; + Path path = getPath(dotPath, bindings); + + MultiValueBinding binding = bindings.getBindingForPath(dotPath); + binding = binding == null ? defaultBinding : binding; + + return binding.bind(path, value); } - private Path getPath(PropertyPath propertyPath, QuerydslBindingContext context) { + private Path getPath(PropertyPath path, QuerydslBindings bindings) { - Path path = context.getPathForPropertyPath(propertyPath); - if (path != null) { - return path; + Path resolvedPath = bindings.getExistingPath(path); + + if (resolvedPath != null) { + return resolvedPath; } - return new PropertyPathPathBuilder(context.getTargetType()).forProperty(propertyPath); + resolvedPath = paths.get(resolvedPath); + + if (resolvedPath != null) { + return resolvedPath; + } + + resolvedPath = reifyPath(path, null); + paths.put(path, resolvedPath); + + return resolvedPath; } - private Object convertToPropertyPathSpecificType(Object source, PropertyPath path, QuerydslBindingContext context) { + private static Path reifyPath(PropertyPath path, EntityPath base) { + + EntityPath entityPath = base != null ? base + : SimpleEntityPathResolver.INSTANCE.createPath(path.getOwningType().getType()); + + Field field = ReflectionUtils.findField(entityPath.getClass(), path.getSegment()); + Object value = ReflectionUtils.getField(field, entityPath); + + if (path.hasNext() && value instanceof EntityPath) { + return reifyPath(path.next(), (EntityPath) value); + } + + return (Path) value; + } + + private Collection convertToPropertyPathSpecificType(Object source, PropertyPath path) { + + Class targetType = path.getLeafProperty().getType(); + + if (targetType.isInstance(source)) { + return Collections.singleton(source); + } Object value = source; if (ObjectUtils.isArray(value)) { - - List list = CollectionUtils.arrayToList(value); - if (!list.isEmpty() && list.size() == 1) { - value = list.get(0); - } else { - value = list; - } + value = CollectionUtils.arrayToList(value); } if (value instanceof Collection) { - return potentiallyConvertCollectionValues((Collection) value, path.getType(), context); + return potentiallyConvertCollectionValues((Collection) value, targetType); } - return potentiallyConvertValue(value, path.getType(), context); + return Collections.singleton(potentiallyConvertValue(value, targetType)); } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private Collection potentiallyConvertCollectionValues(Collection source, Class targetType, - QuerydslBindingContext context) { + private Collection potentiallyConvertCollectionValues(Collection source, Class elementType) { + + Collection target = new ArrayList(source.size()); - Collection target = new ArrayList(source.size()); for (Object value : source) { - target.add(potentiallyConvertValue(value, targetType, context)); + target.add(potentiallyConvertValue(value, elementType)); } return target; } - private Object potentiallyConvertValue(Object source, Class targetType, QuerydslBindingContext context) { - - return context.getConversionService().canConvert(source.getClass(), targetType) - ? context.getConversionService().convert(source, targetType) : source; + private Object potentiallyConvertValue(Object source, Class targetType) { + return conversionService.canConvert(source.getClass(), targetType) ? conversionService.convert(source, targetType) + : source; } - - /** - * {@link PropertyPathPathBuilder} creates a typed {@link Path} based on type information contained in - * {@link PropertyPath}. - * - * @author Christoph Strobl - */ - @SuppressWarnings("rawtypes") - static class PropertyPathPathBuilder { - - final PathBuilder pathBuilder; - - public PropertyPathPathBuilder(Class type) { - pathBuilder = new PathBuilderFactory().create(type); - } - - @SuppressWarnings({ "unchecked" }) - public Path forProperty(PropertyPath source) { - - PathMetadata metadata = PathMetadataFactory.forVariable(source.toDotPath()); - - Path path = null; - if (source.isCollection()) { - path = pathBuilder.get(new CollectionPath(Collection.class, source.getType(), metadata)); - } else if (ClassUtils.isAssignable(String.class, source.getType())) { - path = pathBuilder.get(new StringPath(metadata)); - } else if (ClassUtils.isAssignable(Number.class, source.getType())) { - path = pathBuilder.get(new NumberPath(source.getType(), metadata)); - } else { - path = pathBuilder.get(new SimplePath(source.getType(), metadata)); - } - - return path; - } - } - } diff --git a/src/main/java/org/springframework/data/web/querydsl/SingleValueBinding.java b/src/main/java/org/springframework/data/web/querydsl/SingleValueBinding.java new file mode 100644 index 000000000..4fda5551b --- /dev/null +++ b/src/main/java/org/springframework/data/web/querydsl/SingleValueBinding.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 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.web.querydsl; + +import com.mysema.query.types.Path; +import com.mysema.query.types.Predicate; + +/** + * {@link SingleValueBinding} creates a {@link Predicate} out of given {@link Path} and value. Used for specific parameter + * treatment in {@link QuerydslBindings}. + * + * @author Christoph Strobl + * @author Oliver Gierke + * @since 1.11 + */ +@FunctionalInterface +public interface SingleValueBinding, S> { + + /** + * Returns the predicate to be applied to the given {@link Path} for the given value. + * + * @param path {@link Path} to the property. Must not be {@literal null}. + * @param value the value that should be bound. Can be {@literal null}. + * @return must not be {@literal null}. + */ + Predicate bind(T path, S value); +} diff --git a/src/test/java/org/springframework/data/querydsl/Address.java b/src/test/java/org/springframework/data/querydsl/Address.java index 38bb0a604..703bfabcf 100644 --- a/src/test/java/org/springframework/data/querydsl/Address.java +++ b/src/test/java/org/springframework/data/querydsl/Address.java @@ -22,6 +22,11 @@ import com.mysema.query.annotations.QueryEntity; */ @QueryEntity public class Address { - String street; - String city; + + public String street, city; + + public Address(String street, String city) { + this.street = street; + this.city = city; + } } diff --git a/src/test/java/org/springframework/data/querydsl/User.java b/src/test/java/org/springframework/data/querydsl/User.java index ba7b52a83..567def2b8 100644 --- a/src/test/java/org/springframework/data/querydsl/User.java +++ b/src/test/java/org/springframework/data/querydsl/User.java @@ -28,11 +28,16 @@ import com.mysema.query.annotations.QueryEntity; @QueryEntity public class User { - String firstname; - String lastname; - Date dateOfBirth; + public String firstname, lastname; + public Date dateOfBirth; + public Address address; + public List nickNames; + public Long inceptionYear; - Address address; - List nickNames; - Long inceptionYear; + public User(String firstname, String lastname, Address address) { + + this.firstname = firstname; + this.lastname = lastname; + this.address = address; + } } diff --git a/src/test/java/org/springframework/data/querydsl/Users.java b/src/test/java/org/springframework/data/querydsl/Users.java new file mode 100644 index 000000000..56c1a9f2c --- /dev/null +++ b/src/test/java/org/springframework/data/querydsl/Users.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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.querydsl; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Oliver Gierke + */ +public class Users { + + public static final User OLIVER, CHRISTOPH; + + public static final List USERS; + + static { + + OLIVER = new User("Oliver", "Gierke", new Address("Somewhere", "Dresden")); + CHRISTOPH = new User("Christoph", "Strobl", new Address("Somewhere", "Linz")); + + USERS = Arrays.asList(OLIVER, CHRISTOPH); + } +} diff --git a/src/test/java/org/springframework/data/web/querydsl/QuerydslBindingsUnitTests.java b/src/test/java/org/springframework/data/web/querydsl/QuerydslBindingsUnitTests.java index ba0bdcc76..02f999451 100644 --- a/src/test/java/org/springframework/data/web/querydsl/QuerydslBindingsUnitTests.java +++ b/src/test/java/org/springframework/data/web/querydsl/QuerydslBindingsUnitTests.java @@ -15,21 +15,18 @@ */ package org.springframework.data.web.querydsl; -import static org.hamcrest.core.Is.*; -import static org.hamcrest.core.IsNull.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import java.util.Collections; - import org.junit.Before; import org.junit.Test; -import org.springframework.beans.MutablePropertyValues; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.querydsl.Address; -import org.springframework.data.querydsl.QAddress; +import org.springframework.data.querydsl.QUser; import org.springframework.data.querydsl.User; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.test.util.ReflectionTestUtils; +import com.mysema.query.types.Path; import com.mysema.query.types.Predicate; import com.mysema.query.types.path.StringPath; @@ -41,69 +38,50 @@ import com.mysema.query.types.path.StringPath; */ public class QuerydslBindingsUnitTests { - private QuerydslPredicateBuilder builder; - private QuerydslBindings typeBasedBindings; - private QuerydslBindings pathBasedBindings; + QuerydslPredicateBuilder builder; + QuerydslBindings bindings; + + static final SingleValueBinding CONTAINS_BINDING = new SingleValueBinding() { + + @Override + public Predicate bind(StringPath path, String value) { + return path.contains(value); + } + }; @Before public void setUp() { - builder = new QuerydslPredicateBuilder(); - typeBasedBindings = new QuerydslBindings() { - { - bind(String.class, new QuerydslBinding() { - - @Override - public Predicate bind(StringPath path, Object value) { - return path.contains(value.toString()); - } - }); - } - }; - - pathBasedBindings = new QuerydslBindings() { - { - bind("address.street", new QuerydslBinding() { - - @Override - public Predicate bind(StringPath path, Object value) { - return path.contains(value.toString()); - } - }); - - bind("firstname", new QuerydslBinding() { - - @Override - public Predicate bind(StringPath path, Object value) { - return path.contains(value.toString()); - } - }); - } - }; + this.builder = new QuerydslPredicateBuilder(new DefaultConversionService()); + this.bindings = new QuerydslBindings(); } /** * @see DATACMNS-669 */ @Test(expected = IllegalArgumentException.class) - public void getBindingForPathShouldThrowErrorWhenPathIsNull() { - pathBasedBindings.getBindingForPath(null); + public void rejectsNullPath() { + bindings.getBindingForPath(null); } /** * @see DATACMNS-669 */ @Test - public void getBindingForPathShouldReturnNullWhenNoSpecifcBindingAvailable() { - assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("lastname", User.class)), nullValue()); + public void returnsNullIfNoBindingRegisteredForPath() { + assertThat(bindings.getBindingForPath(PropertyPath.from("lastname", User.class)), nullValue()); } /** * @see DATACMNS-669 */ @Test - public void getBindingForPathShouldReturnSpeficicBindingWhenAvailable() { - assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("firstname", User.class)), notNullValue()); + public void returnsRegisteredBindingForSimplePath() { + + bindings.bind("firstname").using(CONTAINS_BINDING); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("firstname", User.class)), + CONTAINS_BINDING); } /** @@ -111,7 +89,11 @@ public class QuerydslBindingsUnitTests { */ @Test public void getBindingForPathShouldReturnSpeficicBindingForNestedElementsWhenAvailable() { - assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("address.street", User.class)), notNullValue()); + + bindings.bind("address.street").using(CONTAINS_BINDING); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("address.street", User.class)), + CONTAINS_BINDING); } /** @@ -119,100 +101,112 @@ public class QuerydslBindingsUnitTests { */ @Test public void getBindingForPathShouldReturnSpeficicBindingForTypes() { - assertThat(typeBasedBindings.getBindingForPath(PropertyPath.from("address.street", User.class)), notNullValue()); + + bindings.bind(String.class).single(CONTAINS_BINDING); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("address.street", User.class)), + CONTAINS_BINDING); } /** * @see DATACMNS-669 */ @Test - public void getBindingForPathShouldIgnoreSpeficicBindingForTypesWhenTypesDoNotMatch() { - assertThat(typeBasedBindings.getBindingForPath(PropertyPath.from("inceptionYear", User.class)), nullValue()); + public void propertyNotExplicitlyIncludedAndWithoutTypeBindingIsInvisible() { + + bindings.bind(String.class).single(CONTAINS_BINDING); + + assertThat(bindings.getBindingForPath(PropertyPath.from("inceptionYear", User.class)), nullValue()); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnTrueWhenNoRestrictionDefined() { - assertThat(typeBasedBindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true)); + public void pathIsVisibleIfTypeBasedBindingWasRegistered() { + + bindings.bind(String.class).single(CONTAINS_BINDING); + + assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnTrueWhenPathContainedInIncluding() { + public void explicitlyIncludedPathIsVisible() { - assertThat( - new QuerydslBindings().including("inceptionYear").isPathVisible(PropertyPath.from("inceptionYear", User.class)), - is(true)); + bindings.including("inceptionYear"); + + assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnFalseWhenPathNotContainedInIncluding() { + public void notExplicitlyIncludedPathIsInvisible() { - assertThat( - new QuerydslBindings().including("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)), - is(false)); + bindings.including("inceptionYear"); + + assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(false)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnFalseWhenPathContainedInExcluding() { + public void excludedPathIsInvisible() { - assertThat( - new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("inceptionYear", User.class)), - is(false)); + bindings.excluding("inceptionYear"); + + assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(false)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnTrueWhenPathNotContainedInExcluding() { + public void pathIsVisibleIfNotExplicitlyExcluded() { - assertThat( - new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)), - is(true)); + bindings.excluding("inceptionYear"); + + assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(true)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnTrueWhenPathContainedInExcludingAndIncluding() { + public void pathIsVisibleIfItsBothBlackAndWhitelisted() { - assertThat( - new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)), - is(true)); + bindings.excluding("firstname"); + bindings.including("firstname"); + + assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(true)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnFalseWhenPartialPathContainedInExcluding() { + public void nestedPathIsInvisibleIfAParanetPathWasExcluded() { - assertThat( - new QuerydslBindings().excluding("address").isPathVisible(PropertyPath.from("address.city", User.class)), - is(false)); + bindings.excluding("address"); + + assertThat(bindings.isPathVisible(PropertyPath.from("address.city", User.class)), is(false)); } /** * @see DATACMNS-669 */ @Test - public void isPathVisibleShouldReturnTrueWhenPartialPathContainedInExcludingButConcretePathIsIncluded() { + public void pathIsVisibleIfConcretePathIsVisibleButParentExcluded() { - assertThat( - new QuerydslBindings().excluding("address").including("address.city") - .isPathVisible(PropertyPath.from("address.city", User.class)), is(true)); + bindings.excluding("address"); + bindings.including("address.city"); + + assertThat(bindings.isPathVisible(PropertyPath.from("address.city", User.class)), is(true)); } /** @@ -221,22 +215,32 @@ public class QuerydslBindingsUnitTests { @Test public void isPathVisibleShouldReturnFalseWhenPartialPathContainedInExcludingAndConcretePathToDifferentPropertyIsIncluded() { - assertThat( - new QuerydslBindings().excluding("address").including("address.city") - .isPathVisible(PropertyPath.from("address.street", User.class)), is(false)); + bindings.excluding("address"); + bindings.including("address.city"); + + assertThat(bindings.isPathVisible(PropertyPath.from("address.street", User.class)), is(false)); } - /** - * @see DATACMNS-669 - */ @Test - public void usesTypeBasedBindingIfConfigured() { + public void testname() { - MutablePropertyValues values = new MutablePropertyValues(Collections.singletonMap("city", "Dresden")); + PropertyPath firstname = PropertyPath.from("firstname", User.class); + PropertyPath lastname = PropertyPath.from("lastname", User.class); + PropertyPath city = PropertyPath.from("address.city", User.class); + PropertyPath street = PropertyPath.from("address.street", User.class); - QuerydslBindingContext context = new QuerydslBindingContext(ClassTypeInformation.from(Address.class), - this.typeBasedBindings, null); + bindings.including(QUser.user.firstname, QUser.user.address.street); - assertThat(builder.getPredicate(values, context), is((Predicate) QAddress.address.city.contains("Dresden"))); + assertThat(bindings.isPathVisible(firstname), is(true)); + assertThat(bindings.isPathVisible(street), is(true)); + assertThat(bindings.isPathVisible(lastname), is(false)); + assertThat(bindings.isPathVisible(city), is(false)); + } + + private static

, S> void assertAdapterWithTargetBinding(MultiValueBinding binding, + SingleValueBinding, ?> expected) { + + assertThat(binding, is(instanceOf(QuerydslBindings.MultiValueBindingAdapter.class))); + assertThat(ReflectionTestUtils.getField(binding, "delegate"), is((Object) expected)); } } diff --git a/src/test/java/org/springframework/data/web/querydsl/QuerydslDefaultBindingUnitTests.java b/src/test/java/org/springframework/data/web/querydsl/QuerydslDefaultBindingUnitTests.java index c2473bb77..30d6cb7fa 100644 --- a/src/test/java/org/springframework/data/web/querydsl/QuerydslDefaultBindingUnitTests.java +++ b/src/test/java/org/springframework/data/web/querydsl/QuerydslDefaultBindingUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.web.querydsl; import static org.hamcrest.core.Is.*; import java.util.Arrays; +import java.util.Collections; import org.hamcrest.Matcher; import org.hamcrest.core.Is; @@ -47,7 +48,7 @@ public class QuerydslDefaultBindingUnitTests { @Test public void shouldCreatePredicateCorrectlyWhenPropertyIsInRoot() { - Predicate predicate = builder.bind(QUser.user.firstname, "tam"); + Predicate predicate = builder.bind(QUser.user.firstname, Collections.singleton("tam")); assertThat(predicate, is(QUser.user.firstname.eq("tam"))); } @@ -58,7 +59,7 @@ public class QuerydslDefaultBindingUnitTests { @Test public void shouldCreatePredicateCorrectlyWhenPropertyIsInNestedElement() { - Predicate predicate = builder.bind(QUser.user.address.city, "two rivers"); + Predicate predicate = builder.bind(QUser.user.address.city, Collections.singleton("two rivers")); Assert.assertThat(predicate.toString(), is(QUser.user.address.city.eq("two rivers").toString())); } @@ -69,7 +70,7 @@ public class QuerydslDefaultBindingUnitTests { @Test public void shouldCreatePredicateCorrectlyWhenValueIsNull() { - Predicate predicate = builder.bind(QUser.user.firstname, null); + Predicate predicate = builder.bind(QUser.user.firstname, Collections.emptySet()); assertThat(predicate, is(QUser.user.firstname.isNull())); } @@ -80,7 +81,7 @@ public class QuerydslDefaultBindingUnitTests { @Test public void shouldCreatePredicateWithContainingWhenPropertyIsCollectionLikeAndValueIsObject() { - Predicate predicate = builder.bind(QUser.user.nickNames, "dragon reborn"); + Predicate predicate = builder.bind(QUser.user.nickNames, Collections.singleton("dragon reborn")); assertThat(predicate, is(QUser.user.nickNames.contains("dragon reborn"))); } diff --git a/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverUnitTests.java index 5cbb9a0e0..ae9b18d55 100644 --- a/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverUnitTests.java @@ -18,7 +18,6 @@ package org.springframework.data.web.querydsl; import static org.hamcrest.core.Is.*; import static org.junit.Assert.*; -import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Test; import org.springframework.core.MethodParameter; @@ -34,7 +33,10 @@ import com.mysema.query.types.expr.BooleanExpression; import com.mysema.query.types.path.StringPath; /** + * Unit tests for {@link QuerydslPredicateArgumentResolver}. + * * @author Christoph Strobl + * @author Oliver Gierke */ public class QuerydslPredicateArgumentResolverUnitTests { @@ -90,10 +92,10 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("firstname", "rand"); - Object predicate = (BooleanExpression) resolver.resolveArgument( + Predicate predicate = (BooleanExpression) resolver.resolveArgument( getMethodParameterFor("simpleFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate, Is. is(QUser.user.firstname.eq("rand"))); + assertThat(predicate, is((Predicate) QUser.user.firstname.eq("rand"))); } /** @@ -105,10 +107,10 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("firstname", "rand"); request.addParameter("lastname", "al'thor"); - Object predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null, + Predicate predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate, Is. is(QUser.user.firstname.eq("rand").and(QUser.user.lastname.eq("al'thor")))); + assertThat(predicate, is((Predicate) QUser.user.firstname.eq("rand").and(QUser.user.lastname.eq("al'thor")))); } /** @@ -119,10 +121,12 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("address.city", "two rivers"); - Object predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null, + Predicate predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate.toString(), is(QUser.user.address.city.eq("two rivers").toString())); + BooleanExpression eq = QUser.user.address.city.eq("two rivers"); + + assertThat(predicate, is((Predicate) eq)); } /** @@ -133,10 +137,10 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("address.city", "tar valon"); - Object predicate = resolver.resolveArgument(getMethodParameterFor("pagedFind", Predicate.class, Pageable.class), + Predicate predicate = resolver.resolveArgument(getMethodParameterFor("pagedFind", Predicate.class, Pageable.class), null, new ServletWebRequest(request), null); - assertThat(predicate.toString(), is(QUser.user.address.city.eq("tar valon").toString())); + assertThat(predicate, is((Predicate) QUser.user.address.city.eq("tar valon"))); } /** @@ -148,12 +152,11 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("firstname", "egwene"); request.addParameter("lastname", "al'vere"); - Object predicate = resolver.resolveArgument(getMethodParameterFor("specificFind", Predicate.class), null, + Predicate predicate = resolver.resolveArgument(getMethodParameterFor("specificFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate.toString(), - is(QUser.user.firstname.eq("egwene".toUpperCase()).and(QUser.user.lastname.toLowerCase().eq("al'vere")) - .toString())); + assertThat(predicate, is((Predicate) QUser.user.firstname.eq("egwene".toUpperCase()) + .and(QUser.user.lastname.toLowerCase().eq("al'vere")))); } /** @@ -164,10 +167,10 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("inceptionYear", "978"); - Object predicate = (BooleanExpression) resolver.resolveArgument( + Predicate predicate = (BooleanExpression) resolver.resolveArgument( getMethodParameterFor("specificFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate, Is. is(QUser.user.inceptionYear.eq(978L))); + assertThat(predicate, is((Predicate) QUser.user.inceptionYear.eq(978L))); } /** @@ -178,10 +181,10 @@ public class QuerydslPredicateArgumentResolverUnitTests { request.addParameter("inceptionYear", new String[] { "978", "998" }); - Object predicate = (BooleanExpression) resolver.resolveArgument( + Predicate predicate = (BooleanExpression) resolver.resolveArgument( getMethodParameterFor("specificFind", Predicate.class), null, new ServletWebRequest(request), null); - assertThat(predicate, Is. is(QUser.user.inceptionYear.in(978L, 998L))); + assertThat(predicate, is((Predicate) QUser.user.inceptionYear.in(978L, 998L))); } /** @@ -199,7 +202,7 @@ public class QuerydslPredicateArgumentResolverUnitTests { assertThat(predicate.toString(), is(QUser.user.inceptionYear.eq(973L).toString())); } - private MethodParameter getMethodParameterFor(String methodName, Class... args) throws RuntimeException { + private static MethodParameter getMethodParameterFor(String methodName, Class... args) throws RuntimeException { try { return new MethodParameter(Sample.class.getMethod(methodName, args), 0); @@ -212,19 +215,19 @@ public class QuerydslPredicateArgumentResolverUnitTests { public SpecificBinding() { - bind("firstname", new QuerydslBinding() { + bind("firstname").using(new SingleValueBinding() { @Override - public Predicate bind(StringPath path, Object value) { - return path.eq(value.toString().toUpperCase()); + public Predicate bind(StringPath path, String value) { + return path.eq(value.toUpperCase()); } }); - bind(QUser.user.lastname, new QuerydslBinding() { + bind(QUser.user.lastname).single(new SingleValueBinding() { @Override - public Predicate bind(StringPath path, Object value) { - return path.toLowerCase().eq(value.toString()); + public Predicate bind(StringPath path, String value) { + return path.toLowerCase().eq(value); } }); diff --git a/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilderUnitTests.java b/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilderUnitTests.java index 19ef24907..96b9475a8 100644 --- a/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/web/querydsl/QuerydslPredicateBuilderUnitTests.java @@ -15,32 +15,49 @@ */ package org.springframework.data.web.querydsl; -import static org.hamcrest.core.Is.*; +import static java.util.Collections.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.*; -import java.util.Collections; +import java.util.List; -import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.querydsl.QUser; import org.springframework.data.querydsl.User; +import org.springframework.data.querydsl.Users; import org.springframework.data.util.ClassTypeInformation; import com.mysema.query.BooleanBuilder; +import com.mysema.query.collections.CollQueryFactory; import com.mysema.query.types.Predicate; /** + * Unit tests for {@link QuerydslPredicateBuilder}. + * * @author Christoph Strobl + * @author Oliver Gierke */ public class QuerydslPredicateBuilderUnitTests { + static final QuerydslBindings DEFAULT_BINDINGS = new QuerydslBindings(); + QuerydslPredicateBuilder builder; @Before public void setUp() { - builder = new QuerydslPredicateBuilder(); + builder = new QuerydslPredicateBuilder(new DefaultConversionService()); + } + + /** + * @see DATACMNS-669 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullConversionService() { + new QuerydslPredicateBuilder(null); } /** @@ -48,7 +65,7 @@ public class QuerydslPredicateBuilderUnitTests { */ @Test(expected = IllegalArgumentException.class) public void getPredicateShouldThrowErrorWhenBindingContextIsNull() { - builder.getPredicate(new MutablePropertyValues(), null); + builder.getPredicate(new MutablePropertyValues(), null, null); } /** @@ -57,8 +74,8 @@ public class QuerydslPredicateBuilderUnitTests { @Test public void getPredicateShouldReturnEmptyPredicateWhenPropertiesAreEmpty() { - assertThat(builder.getPredicate(new MutablePropertyValues(), new QuerydslBindingContext( - ClassTypeInformation.OBJECT, null, null)), is((Predicate) new BooleanBuilder())); + assertThat(builder.getPredicate(new MutablePropertyValues(), DEFAULT_BINDINGS, ClassTypeInformation.OBJECT), + is((Predicate) new BooleanBuilder())); } /** @@ -67,10 +84,31 @@ public class QuerydslPredicateBuilderUnitTests { @Test public void resolveArgumentShouldCreateSingleStringParameterPredicateCorrectly() throws Exception { - Predicate predicate = builder.getPredicate( - new MutablePropertyValues(Collections.singletonMap("firstname", new String[] { "rand" })), - new QuerydslBindingContext(ClassTypeInformation.from(User.class), null, null)); + Predicate predicate = builder.getPredicate(new MutablePropertyValues(singletonMap("firstname", "Oliver")), + DEFAULT_BINDINGS, ClassTypeInformation.from(User.class)); - assertThat(predicate, Is. is(QUser.user.firstname.eq("rand"))); + assertThat(predicate, is((Predicate) QUser.user.firstname.eq("Oliver"))); + + List result = CollQueryFactory.from(QUser.user, Users.USERS).where(predicate).list(QUser.user); + + assertThat(result, hasSize(1)); + assertThat(result, hasItem(Users.OLIVER)); + } + + /** + * @see DATACMNS-669 + */ + @Test + public void resolveArgumentShouldCreateNestedStringParameterPredicateCorrectly() throws Exception { + + Predicate predicate = builder.getPredicate(new MutablePropertyValues(singletonMap("address.city", "Linz")), + DEFAULT_BINDINGS, ClassTypeInformation.from(User.class)); + + assertThat(predicate, is((Predicate) QUser.user.address.city.eq("Linz"))); + + List result = CollQueryFactory.from(QUser.user, Users.USERS).where(predicate).list(QUser.user); + + assertThat(result, hasSize(1)); + assertThat(result, hasItem(Users.CHRISTOPH)); } }