From d57fb3ea0e713d69d49b469476adb794f95b433f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Jan 2021 15:47:56 +0100 Subject: [PATCH] DATACMNS-1785 - Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce constructors accepting non-null ConversionService, handle conversion service defaulting in configuration classes by using ObjectProvider. Extract common code to create a predicate in getPredicate(…) method. Add unit test. Convert spaces to tabs. Related ticket: #2200. Original pull request: #2274. --- .../web/config/QuerydslWebConfiguration.java | 7 +- .../ReactiveQuerydslWebConfiguration.java | 28 +-- .../QuerydslPredicateArgumentResolver.java | 59 +++--- ...rydslPredicateArgumentResolverSupport.java | 168 +++++++++-------- ...tiveQuerydslPredicateArgumentResolver.java | 64 +++---- ...dslPredicateArgumentResolverUnitTests.java | 174 ++++++++++++++++++ 6 files changed, 350 insertions(+), 150 deletions(-) create mode 100755 src/test/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolverUnitTests.java diff --git a/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java b/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java index baf0c766b..d5c6230e2 100644 --- a/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java @@ -16,10 +16,8 @@ package org.springframework.data.web.config; import java.util.List; -import java.util.Optional; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -27,6 +25,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; 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.querydsl.binding.QuerydslBindingsFactory; @@ -48,7 +47,7 @@ import com.querydsl.core.types.Predicate; @Configuration(proxyBeanMethods = false) public class QuerydslWebConfiguration implements WebMvcConfigurer { - @Autowired @Qualifier("mvcConversionService") ObjectFactory conversionService; + @Autowired @Qualifier("mvcConversionService") ObjectProvider conversionService; @Autowired ObjectProvider resolver; @Autowired BeanFactory beanFactory; @@ -63,7 +62,7 @@ public class QuerydslWebConfiguration implements WebMvcConfigurer { public QuerydslPredicateArgumentResolver querydslPredicateArgumentResolver() { return new QuerydslPredicateArgumentResolver( beanFactory.getBean("querydslBindingsFactory", QuerydslBindingsFactory.class), - Optional.of(conversionService.getObject())); + conversionService.getIfUnique(DefaultConversionService::getSharedInstance)); } @Lazy diff --git a/src/main/java/org/springframework/data/web/config/ReactiveQuerydslWebConfiguration.java b/src/main/java/org/springframework/data/web/config/ReactiveQuerydslWebConfiguration.java index 44173735d..863e3b312 100644 --- a/src/main/java/org/springframework/data/web/config/ReactiveQuerydslWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/ReactiveQuerydslWebConfiguration.java @@ -15,10 +15,7 @@ */ package org.springframework.data.web.config; -import java.util.Optional; - import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -26,6 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; 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.querydsl.binding.QuerydslBindingsFactory; @@ -34,24 +32,24 @@ import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; /** - * Querydsl-specific web configuration for Spring Data. Registers a {@link HandlerMethodArgumentResolver} that builds up - * {@link Predicate}s from web requests. + * Querydsl-specific web configuration for Spring Data. Registers a + * {@link org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver} that builds up + * {@link com.querydsl.core.types.Predicate}s from web requests. * * @author Matías Hermosilla + * @author Mark Paluch * @since 1.11 - * @soundtrack Anika Nilles - Alter Ego */ @Configuration(proxyBeanMethods = false) public class ReactiveQuerydslWebConfiguration implements WebFluxConfigurer { - @Autowired - @Qualifier("webFluxConversionService") ObjectFactory conversionService; + @Autowired @Qualifier("webFluxConversionService") ObjectProvider conversionService; @Autowired ObjectProvider resolver; @Autowired BeanFactory beanFactory; /** - * Default {@link ReactiveQuerydslPredicateArgumentResolver} to create Querydsl {@link Predicate} instances for - * Spring WebFlux controller methods. + * Default {@link ReactiveQuerydslPredicateArgumentResolver} to create Querydsl {@link Predicate} instances for Spring + * WebFlux controller methods. * * @return */ @@ -60,7 +58,7 @@ public class ReactiveQuerydslWebConfiguration implements WebFluxConfigurer { public ReactiveQuerydslPredicateArgumentResolver querydslPredicateArgumentResolver() { return new ReactiveQuerydslPredicateArgumentResolver( beanFactory.getBean("querydslBindingsFactory", QuerydslBindingsFactory.class), - Optional.of(conversionService.getObject())); + conversionService.getIfUnique(DefaultConversionService::getSharedInstance)); } @Lazy @@ -69,10 +67,14 @@ public class ReactiveQuerydslWebConfiguration implements WebFluxConfigurer { return new QuerydslBindingsFactory(resolver.getIfUnique(() -> SimpleEntityPathResolver.INSTANCE)); } + /* + * (non-Javadoc) + * @see org.springframework.web.reactive.config.WebFluxConfigurer#configureArgumentResolvers(org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer) + */ @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - configurer.addCustomResolver(beanFactory.getBean("querydslPredicateArgumentResolver", - ReactiveQuerydslPredicateArgumentResolver.class)); + configurer.addCustomResolver( + beanFactory.getBean("querydslPredicateArgumentResolver", ReactiveQuerydslPredicateArgumentResolver.class)); } } 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 afae5aaac..735746156 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolver.java @@ -16,18 +16,15 @@ package org.springframework.data.web.querydsl; import java.util.Arrays; +import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; -import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; -import org.springframework.data.querydsl.binding.QuerydslPredicate; -import org.springframework.data.util.CastUtils; -import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,13 +43,32 @@ import com.querydsl.core.types.Predicate; * @author Christoph Strobl * @author Oliver Gierke * @author Matías Hermosilla + * @author Mark Paluch * @since 1.11 */ public class QuerydslPredicateArgumentResolver extends QuerydslPredicateArgumentResolverSupport implements HandlerMethodArgumentResolver { + /** + * Create a new {@link QuerydslPredicateArgumentResolver}. + * + * @param factory the {@link QuerydslBindingsFactory} to use, must not be {@literal null}. + * @param conversionService the optional {@link ConversionService} to use, must not be {@literal null}. Defaults to + * {@link DefaultConversionService} if {@link Optional#empty() empty}. + */ public QuerydslPredicateArgumentResolver(QuerydslBindingsFactory factory, Optional conversionService) { + super(factory, conversionService.orElseGet(DefaultConversionService::getSharedInstance)); + } + + /** + * Create a new {@link QuerydslPredicateArgumentResolver}. + * + * @param factory the {@link QuerydslBindingsFactory} to use, must not be {@literal null}. + * @param conversionService the {@link ConversionService} to use, must not be {@literal null}. + * @since 2.5 + */ + public QuerydslPredicateArgumentResolver(QuerydslBindingsFactory factory, ConversionService conversionService) { super(factory, conversionService); } @@ -65,25 +81,8 @@ public class QuerydslPredicateArgumentResolver extends QuerydslPredicateArgument public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - - for (Entry entry : webRequest.getParameterMap().entrySet()) { - parameters.put(entry.getKey(), Arrays.asList(entry.getValue())); - } - - Optional annotation = Optional - .ofNullable(parameter.getParameterAnnotation(QuerydslPredicate.class)); - TypeInformation domainType = extractTypeInfo(parameter).getRequiredActualType(); - - Optional>> bindingsAnnotation = annotation // - .map(QuerydslPredicate::bindings) // - .map(CastUtils::cast); - - QuerydslBindings bindings = bindingsAnnotation // - .map(it -> bindingsFactory.createBindingsFor(domainType, it)) // - .orElseGet(() -> bindingsFactory.createBindingsFor(domainType)); - - Predicate result = predicateBuilder.getPredicate(domainType, parameters, bindings); + MultiValueMap queryParameters = getQueryParameters(webRequest); + Predicate result = getPredicate(parameter, queryParameters); if (!parameter.isOptional() && result == null) { return new BooleanBuilder(); @@ -94,4 +93,16 @@ public class QuerydslPredicateArgumentResolver extends QuerydslPredicateArgument : result; } + private static MultiValueMap getQueryParameters(NativeWebRequest webRequest) { + + Map parameterMap = webRequest.getParameterMap(); + MultiValueMap queryParameters = new LinkedMultiValueMap<>(parameterMap.size()); + + for (Entry entry : parameterMap.entrySet()) { + queryParameters.put(entry.getKey(), Arrays.asList(entry.getValue())); + } + + return queryParameters; + } + } diff --git a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverSupport.java b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverSupport.java index 7f05649d3..e7d5ef7a9 100644 --- a/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverSupport.java +++ b/src/main/java/org/springframework/data/web/querydsl/QuerydslPredicateArgumentResolverSupport.java @@ -21,12 +21,17 @@ import java.util.Optional; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; import org.springframework.data.querydsl.binding.QuerydslPredicate; import org.springframework.data.querydsl.binding.QuerydslPredicateBuilder; +import org.springframework.data.util.CastUtils; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import com.querydsl.core.types.Predicate; @@ -38,101 +43,120 @@ import com.querydsl.core.types.Predicate; * @author Christoph Strobl * @author Oliver Gierke * @author Matías Hermosilla - * @since 1.11 + * @since 2.5 */ public abstract class QuerydslPredicateArgumentResolverSupport { - private static final ResolvableType PREDICATE = ResolvableType.forClass(Predicate.class); - protected static final ResolvableType OPTIONAL_OF_PREDICATE = ResolvableType.forClassWithGenerics(Optional.class, - PREDICATE); + private static final ResolvableType PREDICATE = ResolvableType.forClass(Predicate.class); - protected final QuerydslBindingsFactory bindingsFactory; - protected final QuerydslPredicateBuilder predicateBuilder; + static final ResolvableType OPTIONAL_OF_PREDICATE = ResolvableType.forClassWithGenerics(Optional.class, PREDICATE); - /** - * Creates a new {@link QuerydslPredicateArgumentResolver} using the given {@link ConversionService}. - * - * @param factory - * @param conversionService defaults to {@link DefaultConversionService} if {@literal null}. - */ - public QuerydslPredicateArgumentResolverSupport(QuerydslBindingsFactory factory, - Optional conversionService) { + protected final QuerydslBindingsFactory bindingsFactory; + protected final QuerydslPredicateBuilder predicateBuilder; - this.bindingsFactory = factory; - this.predicateBuilder = new QuerydslPredicateBuilder(conversionService.orElseGet(DefaultConversionService::new), - factory.getEntityPathResolver()); - } + /** + * Creates a new {@link QuerydslPredicateArgumentResolver} using the given {@link ConversionService}. + * + * @param factory + * @param conversionService + */ + protected QuerydslPredicateArgumentResolverSupport(QuerydslBindingsFactory factory, + ConversionService conversionService) { - /* - * (non-Javadoc) - * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) - */ - public boolean supportsParameter(MethodParameter parameter) { + Assert.notNull(factory, "QuerydslBindingsFactory must not be null"); + Assert.notNull(conversionService, "ConversionService must not be null"); - ResolvableType type = ResolvableType.forMethodParameter(parameter); + this.bindingsFactory = factory; + this.predicateBuilder = new QuerydslPredicateBuilder(conversionService, factory.getEntityPathResolver()); + } - if (PREDICATE.isAssignableFrom(type) || OPTIONAL_OF_PREDICATE.isAssignableFrom(type)) { - return true; - } + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + public boolean supportsParameter(MethodParameter parameter) { - 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())); - } + ResolvableType type = ResolvableType.forMethodParameter(parameter); - return false; - } + if (PREDICATE.isAssignableFrom(type) || OPTIONAL_OF_PREDICATE.isAssignableFrom(type)) { + return true; + } - /** - * 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 - */ - protected static TypeInformation extractTypeInfo(MethodParameter parameter) { + 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())); + } - Optional annotation = Optional - .ofNullable(parameter.getParameterAnnotation(QuerydslPredicate.class)); + return false; + } - return annotation.filter(it -> !Object.class.equals(it.root()))// - .> map(it -> ClassTypeInformation.from(it.root()))// - .orElseGet(() -> detectDomainType(parameter)); - } + @Nullable + Predicate getPredicate(MethodParameter parameter, MultiValueMap queryParameters) { - private static TypeInformation detectDomainType(MethodParameter parameter) { + Optional annotation = Optional + .ofNullable(parameter.getParameterAnnotation(QuerydslPredicate.class)); + TypeInformation domainType = extractTypeInfo(parameter).getRequiredActualType(); - Method method = parameter.getMethod(); + Optional>> bindingsAnnotation = annotation // + .map(QuerydslPredicate::bindings) // + .map(CastUtils::cast); - if (method == null) { - throw new IllegalArgumentException("Method parameter is not backed by a method!"); - } + QuerydslBindings bindings = bindingsAnnotation // + .map(it -> bindingsFactory.createBindingsFor(domainType, it)) // + .orElseGet(() -> bindingsFactory.createBindingsFor(domainType)); - return detectDomainType(ClassTypeInformation.fromReturnTypeOf(method)); - } + return predicateBuilder.getPredicate(domainType, queryParameters, bindings); + } - private static TypeInformation detectDomainType(TypeInformation source) { + /** + * 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 + */ + protected static TypeInformation extractTypeInfo(MethodParameter parameter) { - if (source.getTypeArguments().isEmpty()) { - return source; - } + Optional annotation = Optional + .ofNullable(parameter.getParameterAnnotation(QuerydslPredicate.class)); - TypeInformation actualType = source.getActualType(); + return annotation.filter(it -> !Object.class.equals(it.root()))// + .> map(it -> ClassTypeInformation.from(it.root()))// + .orElseGet(() -> detectDomainType(parameter)); + } - if (actualType == null) { - throw new IllegalArgumentException(String.format("Could not determine domain type from %s!", source)); - } + private static TypeInformation detectDomainType(MethodParameter parameter) { - if (source != actualType) { - return detectDomainType(actualType); - } + Method method = parameter.getMethod(); - if (source instanceof Iterable) { - return source; - } + if (method == null) { + throw new IllegalArgumentException("Method parameter is not backed by a method!"); + } - return detectDomainType(source.getRequiredComponentType()); - } + return detectDomainType(ClassTypeInformation.fromReturnTypeOf(method)); + } + + private static TypeInformation detectDomainType(TypeInformation source) { + + if (source.getTypeArguments().isEmpty()) { + return source; + } + + TypeInformation actualType = source.getActualType(); + + if (actualType == null) { + throw new IllegalArgumentException(String.format("Could not determine domain type from %s!", source)); + } + + if (source != actualType) { + return detectDomainType(actualType); + } + + if (source instanceof Iterable) { + return source; + } + + return detectDomainType(source.getRequiredComponentType()); + } } diff --git a/src/main/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolver.java b/src/main/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolver.java index fe69e3d21..b07204520 100644 --- a/src/main/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2021 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. @@ -19,76 +19,66 @@ import java.util.List; import java.util.Map.Entry; import java.util.Optional; -import com.querydsl.core.BooleanBuilder; -import com.querydsl.core.types.Predicate; - import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; -import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; -import org.springframework.data.querydsl.binding.QuerydslBindings; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; -import org.springframework.data.querydsl.binding.QuerydslPredicate; -import org.springframework.data.util.CastUtils; -import org.springframework.data.util.TypeInformation; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Predicate; /** * {@link HandlerMethodArgumentResolver} to allow injection of {@link com.querydsl.core.types.Predicate} into Spring * WebFlux controller methods. * * @author Matías Hermosilla - * @since 1.11 + * @author Mark Paluch + * @since 2.5 */ public class ReactiveQuerydslPredicateArgumentResolver extends QuerydslPredicateArgumentResolverSupport - implements HandlerMethodArgumentResolver { + implements SyncHandlerMethodArgumentResolver { public ReactiveQuerydslPredicateArgumentResolver(QuerydslBindingsFactory factory, - Optional conversionService) { + ConversionService conversionService) { super(factory, conversionService); } /* * (non-Javadoc) - * - * @seeorg.springframework.web.reactive.result.method.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.reactive.BindingContext, org.springframework.web.server.ServerWebExchange) + * @see org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver(org.springframework.core.MethodParameter, org.springframework.web.reactive.BindingContext, org.springframework.web.server.ServerWebExchange) */ @Override - public Mono resolveArgument(MethodParameter parameter, BindingContext bindingContext, + public Object resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) { - MultiValueMap parameters = new LinkedMultiValueMap<>(); - for (Entry> entry : exchange.getRequest().getQueryParams().entrySet()) { - parameters.put(entry.getKey(), entry.getValue()); - } - - Optional annotation = Optional - .ofNullable(parameter.getParameterAnnotation(QuerydslPredicate.class)); - TypeInformation domainType = extractTypeInfo(parameter).getRequiredActualType(); - - Optional>> bindingsAnnotation = annotation // - .map(QuerydslPredicate::bindings) // - .map(CastUtils::cast); - - QuerydslBindings bindings = bindingsAnnotation // - .map(it -> bindingsFactory.createBindingsFor(domainType, it)) // - .orElseGet(() -> bindingsFactory.createBindingsFor(domainType)); - - Predicate result = predicateBuilder.getPredicate(domainType, parameters, bindings); + MultiValueMap queryParameters = getQueryParameters(exchange); + Predicate result = getPredicate(parameter, queryParameters); if (!parameter.isOptional() && result == null) { - return Mono.just(new BooleanBuilder()); + return new BooleanBuilder(); } return OPTIONAL_OF_PREDICATE.isAssignableFrom(ResolvableType.forMethodParameter(parameter)) // - ? Mono.justOrEmpty(Optional.ofNullable(result)) // - : Mono.justOrEmpty(result); + ? Optional.ofNullable(result) // + : result; + } + + private static MultiValueMap getQueryParameters(ServerWebExchange exchange) { + + MultiValueMap queryParams = exchange.getRequest().getQueryParams(); + MultiValueMap parameters = new LinkedMultiValueMap<>(queryParams.size()); + + for (Entry> entry : queryParams.entrySet()) { + parameters.put(entry.getKey(), entry.getValue()); + } + + return parameters; } } diff --git a/src/test/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolverUnitTests.java new file mode 100755 index 000000000..e26ed5651 --- /dev/null +++ b/src/test/java/org/springframework/data/web/querydsl/ReactiveQuerydslPredicateArgumentResolverUnitTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2015-2021 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 + * + * https://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 static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.querydsl.QUser; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.querydsl.User; +import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer; +import org.springframework.data.querydsl.binding.QuerydslBindings; +import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; +import org.springframework.data.querydsl.binding.QuerydslPredicate; +import org.springframework.lang.Nullable; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import com.querydsl.core.types.Predicate; + +/** + * Unit tests for {@link ReactiveQuerydslPredicateArgumentResolver}. + * + * @author Mark Paluch + */ +class ReactiveQuerydslPredicateArgumentResolverUnitTests { + + ReactiveQuerydslPredicateArgumentResolver resolver; + + @BeforeEach + void setUp() { + + resolver = new ReactiveQuerydslPredicateArgumentResolver( + new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE), DefaultConversionService.getSharedInstance()); + } + + @Test // DATACMNS-1785 + void supportsParameterReturnsTrueWhenMethodParameterIsPredicateButNotAnnotatedAsSuch() { + assertThat(resolver.supportsParameter(getMethodParameterFor("predicateWithoutAnnotation", Predicate.class))) + .isTrue(); + } + + @Test // DATACMNS-1785 + void supportsParameterShouldThrowExceptionWhenMethodParameterIsNoPredicateButAnnotatedAsSuch() { + assertThatIllegalArgumentException().isThrownBy( + () -> resolver.supportsParameter(getMethodParameterFor("nonPredicateWithAnnotation", String.class))); + } + + @Test // DATACMNS-1785 + void supportsParameterReturnsFalseWhenMethodParameterIsNoPredicate() { + assertThat(resolver.supportsParameter(getMethodParameterFor("nonPredicateWithoutAnnotation", String.class))) + .isFalse(); + } + + @Test // DATACMNS-1785 + void resolveArgumentShouldCreateSingleStringParameterPredicateCorrectly() { + + MockServerHttpRequest request = MockServerHttpRequest.get("").queryParam("firstname", "rand").build(); + + Object predicate = resolver.resolveArgumentValue(getMethodParameterFor("simpleFind", Predicate.class), null, + MockServerWebExchange.from(request)); + + assertThat(predicate).isEqualTo(QUser.user.firstname.eq("rand")); + } + + @Test // DATACMNS-1785 + void resolveArgumentShouldHonorCustomSpecification() { + + MockServerHttpRequest request = MockServerHttpRequest.get("").queryParam("firstname", "egwene") + .queryParam("lastname", "al'vere").build(); + + Object predicate = resolver.resolveArgumentValue(getMethodParameterFor("specificFind", Predicate.class), null, + MockServerWebExchange.from(request)); + + assertThat(predicate).isEqualTo( + QUser.user.firstname.eq("egwene".toUpperCase()).and(QUser.user.lastname.toLowerCase().eq("al'vere"))); + } + + @Test // DATACMNS-1785 + void returnsEmptyPredicateForEmptyInput() { + + MethodParameter parameter = getMethodParameterFor("predicateWithoutAnnotation", Predicate.class); + + MockServerHttpRequest request = MockServerHttpRequest.get("").queryParam("firstname", "").build(); + + assertThat(resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request))) // + .isNotNull(); + } + + @Test // DATACMNS-1785 + void forwardsNullValueForNullablePredicate() { + + MethodParameter parameter = getMethodParameterFor("nullablePredicateWithoutAnnotation", Predicate.class); + + MockServerHttpRequest request = MockServerHttpRequest.get("").queryParam("firstname", "").build(); + + assertThat(resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request))).isNull(); + } + + @Test // DATACMNS-1785 + void returnsOptionalIfDeclared() { + + MethodParameter parameter = getMethodParameterFor("optionalPredicateWithoutAnnotation", Optional.class); + + MockServerHttpRequest request = MockServerHttpRequest.get("").queryParam("firstname", "").build(); + + assertThat(resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request))) // + .isInstanceOfSatisfying(Optional.class, it -> assertThat(it).isEmpty()); + + request = MockServerHttpRequest.get("").queryParam("firstname", "Matthews").build(); + + assertThat(resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request))) // + .isInstanceOfSatisfying(Optional.class, it -> assertThat(it).isPresent()); + } + + private static MethodParameter getMethodParameterFor(String methodName, Class... args) throws RuntimeException { + + try { + return new MethodParameter(Sample.class.getMethod(methodName, args), args.length == 0 ? -1 : 0); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + static class SpecificBinding implements QuerydslBinderCustomizer { + + public void customize(QuerydslBindings bindings, QUser user) { + + bindings.bind(user.firstname).firstOptional((path, value) -> value.map(it -> path.eq(it.toUpperCase()))); + bindings.bind(user.lastname).first((path, value) -> path.toLowerCase().eq(value)); + + bindings.excluding(user.address); + } + } + + interface Sample { + + User predicateWithoutAnnotation(Predicate predicate); + + User nonPredicateWithAnnotation(@QuerydslPredicate String predicate); + + User nonPredicateWithoutAnnotation(String predicate); + + User simpleFind(@QuerydslPredicate Predicate predicate); + + User specificFind(@QuerydslPredicate(bindings = SpecificBinding.class) Predicate predicate); + + // Nullability + + User nullablePredicateWithoutAnnotation(@Nullable Predicate predicate); + + User optionalPredicateWithoutAnnotation(Optional predicate); + } + +}