From 4d9126a474ca9a06ce53740db509c59cc59b3f6b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 2 May 2016 13:54:27 -0400 Subject: [PATCH] Add support for more annotated method arguments - @PathVariable - @RequestHeader - @RequestParam - @CookieValue - @Value - @RequestAttribute - @SessionAttribute --- spring-web-reactive/build.gradle | 2 +- ...tractNamedValueMethodArgumentResolver.java | 290 ++++++++++++++++++ .../CookieValueMethodArgumentResolver.java | 93 ++++++ ...ExpressionValueMethodArgumentResolver.java | 81 +++++ ...PathVariableMapMethodArgumentResolver.java | 64 ++++ .../PathVariableMethodArgumentResolver.java | 114 +++++++ ...equestAttributeMethodArgumentResolver.java | 68 ++++ ...equestHeaderMapMethodArgumentResolver.java | 62 ++++ .../RequestHeaderMethodArgumentResolver.java | 97 ++++++ .../RequestMappingHandlerAdapter.java | 57 +++- .../RequestParamArgumentResolver.java | 52 ---- ...RequestParamMapMethodArgumentResolver.java | 69 +++++ .../RequestParamMethodArgumentResolver.java | 125 ++++++++ ...essionAttributeMethodArgumentResolver.java | 71 +++++ .../web/server/ServerErrorException.java | 65 ++++ .../method/InvocableHandlerMethodTests.java | 8 +- ...equestAttributesArgumentResolverTests.java | 177 +++++++++++ ...ookieValueMethodArgumentResolverTests.java | 132 ++++++++ ...ssionValueMethodArgumentResolverTests.java | 104 +++++++ ...ariableMapMethodArgumentResolverTests.java | 112 +++++++ ...thVariableMethodArgumentResolverTests.java | 110 +++++++ ...tAttributeMethodArgumentResolverTests.java | 163 ++++++++++ ...tHeaderMapMethodArgumentResolverTests.java | 150 +++++++++ ...uestHeaderMethodArgumentResolverTests.java | 229 ++++++++++++++ ...stParamMapMethodArgumentResolverTests.java | 129 ++++++++ ...questParamMethodArgumentResolverTests.java | 232 ++++++++++++++ ...nAttributeMethodArgumentResolverTests.java | 170 ++++++++++ 27 files changed, 2960 insertions(+), 66 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java delete mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java diff --git a/spring-web-reactive/build.gradle b/spring-web-reactive/build.gradle index 76c8db41f9..b0f5771525 100644 --- a/spring-web-reactive/build.gradle +++ b/spring-web-reactive/build.gradle @@ -27,7 +27,7 @@ repositories { } ext { - springVersion = '4.3.0.RC1' + springVersion = '4.3.0.BUILD-SNAPSHOT' reactorVersion = '2.5.0.BUILD-SNAPSHOT' reactorNettyVersion = '2.5.0.BUILD-SNAPSHOT' tomcatVersion = '8.0.33' diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java new file mode 100644 index 0000000000..1db3ca782a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -0,0 +1,290 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.ConversionNotSupportedException; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Abstract base class for resolving method arguments from a named value. + * Request parameters, request headers, and path variables are examples of named + * values. Each may have a name, a required flag, and a default value. + *

Subclasses define how to do the following: + *

+ *

A default value string can contain ${...} placeholders and Spring Expression + * Language #{...} expressions. For this to work a + * {@link ConfigurableBeanFactory} must be supplied to the class constructor. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final ConfigurableBeanFactory configurableBeanFactory; + + private final BeanExpressionContext expressionContext; + + private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); + + /** Instead of a WebDataBinder for now */ + private final SimpleTypeConverter typeConverter; + + + /** + * @param conversionService for type conversion (to be replaced with WebDataBinder) + * @param beanFactory a bean factory to use for resolving ${...} placeholder + * and #{...} SpEL expressions in default values, or {@code null} if default + * values are not expected to contain expressions + */ + public AbstractNamedValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + Assert.notNull(conversionService, "'conversionService' is required."); + this.typeConverter = new SimpleTypeConverter(); + this.typeConverter.setConversionService(conversionService); + this.configurableBeanFactory = beanFactory; + this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null); + } + + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveStringValue(namedValueInfo.name); + if (resolvedName == null) { + return Mono.error(new IllegalArgumentException( + "Specified name must not resolve to null: [" + namedValueInfo.name + "]")); + } + + return resolveName(resolvedName.toString(), nestedParameter, exchange) + .map(arg -> { + if ("".equals(arg) && namedValueInfo.defaultValue != null) { + arg = resolveStringValue(namedValueInfo.defaultValue); + } + arg = applyConversion(arg, parameter); + handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange); + return arg; + }) + .otherwiseIfEmpty(getDefaultValue(namedValueInfo, parameter, model, exchange)); + } + + /** + * Obtain the named value for the given method parameter. + */ + private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { + NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); + if (namedValueInfo == null) { + namedValueInfo = createNamedValueInfo(parameter); + namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); + this.namedValueInfoCache.put(parameter, namedValueInfo); + } + return namedValueInfo; + } + + /** + * Create the {@link NamedValueInfo} object for the given method parameter. + * Implementations typically retrieve the method annotation by means of + * {@link MethodParameter#getParameterAnnotation(Class)}. + * @param parameter the method parameter + * @return the named value information + */ + protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter); + + /** + * Create a new NamedValueInfo based on the given NamedValueInfo with + * sanitized values. + */ + private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { + String name = info.name; + if (info.name.length() == 0) { + name = parameter.getParameterName(); + if (name == null) { + String type = parameter.getNestedParameterType().getName(); + throw new IllegalArgumentException("Name for argument type [" + type + "] not " + + "available, and parameter name information not found in class file either."); + } + } + String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); + return new NamedValueInfo(name, info.required, defaultValue); + } + + /** + * Resolve the given annotation-specified value, + * potentially containing placeholders and expressions. + */ + private Object resolveStringValue(String value) { + if (this.configurableBeanFactory == null) { + return value; + } + String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value); + BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver(); + if (exprResolver == null) { + return value; + } + return exprResolver.evaluate(placeholdersResolved, this.expressionContext); + } + + /** + * Resolve the given parameter type and value name into an argument value. + * @param name the name of the value being resolved + * @param parameter the method parameter to resolve to an argument value + * (pre-nested in case of a {@link java.util.Optional} declaration) + * @param exchange the current exchange + * @return the resolved argument (may be {@code null}) + */ + protected abstract Mono resolveName(String name, MethodParameter parameter, + ServerWebExchange exchange); + + private Object applyConversion(Object value, MethodParameter parameter) { + try { + value = this.typeConverter.convertIfNecessary(value, parameter.getParameterType(), parameter); + } + catch (ConversionNotSupportedException ex) { + throw new ServerErrorException("Conversion not supported.", parameter, ex); + } + catch (TypeMismatchException ex) { + throw new ServerWebInputException("Type mismatch.", parameter, ex); + } + return value; + } + + private Mono getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + + Object value = null; + try { + if (namedValueInfo.defaultValue != null) { + value = resolveStringValue(namedValueInfo.defaultValue); + } + else if (namedValueInfo.required && !parameter.isOptional()) { + handleMissingValue(namedValueInfo.name, parameter, exchange); + } + value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); + value = applyConversion(value, parameter); + handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange); + return Mono.justOrEmpty(value); + } + catch (Throwable ex) { + return Mono.error(ex); + } + } + + /** + * Invoked when a named value is required, but + * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned + * {@code null} and there is no default value. Subclasses typically throw an + * exception in this case. + * @param name the name for the value + * @param parameter the method parameter + * @param exchange the current exchange + */ + @SuppressWarnings("UnusedParameters") + protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) { + handleMissingValue(name, parameter); + } + + /** + * Invoked when a named value is required, but + * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned + * {@code null} and there is no default value. Subclasses typically throw an + * exception in this case. + * @param name the name for the value + * @param parameter the method parameter + */ + protected void handleMissingValue(String name, MethodParameter parameter) { + String typeName = parameter.getNestedParameterType().getSimpleName(); + throw new ServerWebInputException("Missing argument '" + name + "' for method " + + "parameter of type " + typeName, parameter); + } + + /** + * A {@code null} results in a {@code false} value for {@code boolean}s or + * an exception for other primitives. + */ + private Object handleNullValue(String name, Object value, Class paramType) { + if (value == null) { + if (Boolean.TYPE.equals(paramType)) { + return Boolean.FALSE; + } + else if (paramType.isPrimitive()) { + throw new IllegalStateException("Optional " + paramType.getSimpleName() + + " parameter '" + name + "' is present but cannot be translated into a" + + " null value due to being declared as a primitive type. " + + "Consider declaring it as object wrapper for the corresponding primitive type."); + } + } + return value; + } + + /** + * Invoked after a value is resolved. + * @param arg the resolved argument value + * @param name the argument name + * @param parameter the argument parameter type + * @param model the model + * @param exchange the current exchange + */ + @SuppressWarnings("UnusedParameters") + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + } + + + /** + * Represents the information about a named value, including name, whether + * it's required and a default value. + */ + protected static class NamedValueInfo { + + private final String name; + + private final boolean required; + + private final String defaultValue; + + public NamedValueInfo(String name, boolean required, String defaultValue) { + this.name = name; + this.required = required; + this.defaultValue = defaultValue; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java new file mode 100644 index 0000000000..48f239f9cc --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolver.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpCookie; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolve method arguments annotated with {@code @CookieValue}. + * + *

An {@code @CookieValue} is a named value that is resolved from a cookie. + * It has a required flag and a default value to fall back on when the cookie + * does not exist. + * + * @author Rossen Stoyanchev + */ +public class CookieValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to contain expressions + */ + public CookieValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(CookieValue.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + CookieValue annotation = parameter.getParameterAnnotation(CookieValue.class); + return new CookieValueNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + HttpCookie cookie = exchange.getRequest().getCookies().getFirst(name); + if (HttpCookie.class.isAssignableFrom(parameter.getNestedParameterType())) { + return Mono.justOrEmpty(cookie); + } + else if (cookie != null) { + return Mono.justOrEmpty(cookie.getValue()); + } + else { + return Mono.empty(); + } + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing cookie '" + name + "' for method parameter of type " + type; + throw new ServerWebInputException(reason, parameter); + } + + + private static class CookieValueNamedValueInfo extends NamedValueInfo { + + private CookieValueNamedValueInfo(CookieValue annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java new file mode 100644 index 0000000000..fb6f22d659 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolver.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves method arguments annotated with {@code @Value}. + * + *

An {@code @Value} does not have a name but gets resolved from the default + * value string, which may contain ${...} placeholder or Spring Expression + * Language #{...} expressions. + * + * @author Rossen Stoyanchev + */ +public class ExpressionValueMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to contain expressions + */ + public ExpressionValueMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Value.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + Value annotation = parameter.getParameterAnnotation(Value.class); + return new ExpressionValueNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + // No name to resolve + return Mono.empty(); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + throw new UnsupportedOperationException("@Value is never required: " + parameter.getMethod()); + } + + + private static class ExpressionValueNamedValueInfo extends NamedValueInfo { + + private ExpressionValueNamedValueInfo(Value annotation) { + super("@Value", false, annotation.value()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java new file mode 100644 index 0000000000..9488bfa69c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolver.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolver for {@link Map} method arguments also annotated with + * {@link PathVariable @PathVariable} where the annotation does not specify a + * path variable name. The resulting {@link Map} argument is a coyp of all URI + * template name-value pairs. + * + * @author Rossen Stoyanchev + * @see PathVariableMethodArgumentResolver + */ +public class PathVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + PathVariable ann = parameter.getParameterAnnotation(PathVariable.class); + return (ann != null && (Map.class.isAssignableFrom(parameter.getParameterType())) + && !StringUtils.hasText(ann.value())); + } + + /** + * Return a Map with all URI template variables or an empty map. + */ + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, + ServerWebExchange exchange) { + + String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + Optional value = exchange.getAttribute(name); + return (value.isPresent() ? Mono.just(value.get()) : Mono.just(Collections.emptyMap())); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java new file mode 100644 index 0000000000..fd59d53e7c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Map; +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves method arguments annotated with @{@link PathVariable}. + * + *

An @{@link PathVariable} is a named value that gets resolved from a URI + * template variable. It is always required and does not have a default value + * to fall back on. See the base class + * {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} + * for more information on how named values are processed. + * + *

If the method parameter type is {@link Map}, the name specified in the + * annotation is used to resolve the URI variable String value. The value is + * then converted to a {@link Map} via type conversion, assuming a suitable + * {@link Converter}. + * + * @author Rossen Stoyanchev + * @see PathVariableMapMethodArgumentResolver + */ +public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public PathVariableMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + if (!parameter.hasParameterAnnotation(PathVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + String paramName = parameter.getParameterAnnotation(PathVariable.class).value(); + return StringUtils.hasText(paramName); + } + return true; + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class); + return new PathVariableNamedValueInfo(annotation); + } + + @Override + @SuppressWarnings("unchecked") + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + String attributeName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + Optional optional = exchange.getAttribute(attributeName); + Object value = null; + if (optional.isPresent()) { + value = ((Map) optional.get()).get(name); + } + return Mono.justOrEmpty(value); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + throw new ServerErrorException(name, parameter); + } + + @Override + @SuppressWarnings("unchecked") + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, + ModelMap model, ServerWebExchange exchange) { + + // TODO: View.PATH_VARIABLES ? + } + + + private static class PathVariableNamedValueInfo extends NamedValueInfo { + + public PathVariableNamedValueInfo(PathVariable annotation) { + super(annotation.value(), true, ValueConstants.DEFAULT_NONE); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java new file mode 100644 index 0000000000..f14e9fdd35 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with an @{@link RequestAttribute}. + * + * @author Rossen Stoyanchev + * @see SessionAttributeMethodArgumentResolver + */ +public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public RequestAttributeMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestAttribute.class); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class); + return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange){ + return Mono.justOrEmpty(exchange.getAttribute(name)); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing request attribute '" + name + "' of type " + type; + throw new ServerWebInputException(reason, parameter); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java new file mode 100644 index 0000000000..4f3820e9c2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolves {@link Map} method arguments annotated with {@code @RequestHeader}. + * For individual header values annotated with {@code @RequestHeader} see + * {@link RequestHeaderMethodArgumentResolver} instead. + * + *

The created {@link Map} contains all request header name/value pairs. + * The method parameter type may be a {@link MultiValueMap} to receive all + * values for a header, not only the first one. + * + * @author Rossen Stoyanchev + * @see RequestHeaderMethodArgumentResolver + */ +public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(RequestHeader.class) && + Map.class.isAssignableFrom(parameter.getParameterType())); + } + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + HttpHeaders headers = exchange.getRequest().getHeaders(); + if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { + return Mono.just(headers); + } + else { + return Mono.just(headers.toSingleValueMap()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java new file mode 100644 index 0000000000..4f3cd089c5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with {@code @RequestHeader} except for + * {@link Map} arguments. See {@link RequestHeaderMapMethodArgumentResolver} for + * details on {@link Map} arguments annotated with {@code @RequestHeader}. + * + *

An {@code @RequestHeader} is a named value resolved from a request header. + * It has a required flag and a default value to fall back on when the request + * header does not exist. + * + *

A {@link ConversionService} is invoked to apply type conversion to resolved + * request header values that don't yet match the method parameter type. + * + * @author Rossen Stoyanchev + * @see RequestHeaderMapMethodArgumentResolver + */ +public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + /** + * @param beanFactory a bean factory to use for resolving ${...} + * placeholder and #{...} SpEL expressions in default values; + * or {@code null} if default values are not expected to have expressions + */ + public RequestHeaderMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(RequestHeader.class) && + !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestHeader annotation = parameter.getParameterAnnotation(RequestHeader.class); + return new RequestHeaderNamedValueInfo(annotation); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + List headerValues = exchange.getRequest().getHeaders().get(name); + Object result = null; + if (headerValues != null) { + result = (headerValues.size() == 1 ? headerValues.get(0) : headerValues); + } + return Mono.justOrEmpty(result); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + throw new ServerWebInputException("Missing request header '" + name + + "' for method parameter of type " + type); + } + + + private static class RequestHeaderNamedValueInfo extends NamedValueInfo { + + private RequestHeaderNamedValueInfo(RequestHeader annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index d9d2527e62..b6c0ec6848 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -28,7 +28,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.codec.support.ByteBufferDecoder; import org.springframework.core.codec.support.ByteBufferEncoder; import org.springframework.core.codec.support.JacksonJsonDecoder; @@ -57,7 +61,7 @@ import org.springframework.web.server.ServerWebExchange; /** * @author Rossen Stoyanchev */ -public class RequestMappingHandlerAdapter implements HandlerAdapter, InitializingBean { +public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactoryAware, InitializingBean { private static Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); @@ -69,6 +73,9 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); + private ConfigurableBeanFactory beanFactory; + + /** * Configure the complete list of supported argument types thus overriding @@ -94,24 +101,52 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin return this.conversionService; } + /** + * A {@link ConfigurableBeanFactory} is expected for resolving expressions + * in method argument default values. + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableBeanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + } + + public ConfigurableBeanFactory getBeanFactory() { + return beanFactory; + } + + @Override public void afterPropertiesSet() throws Exception { if (ObjectUtils.isEmpty(this.argumentResolvers)) { - List> messageConverters = Arrays.asList( - new CodecHttpMessageConverter(new ByteBufferEncoder(), - new ByteBufferDecoder()), - new CodecHttpMessageConverter(new StringEncoder(), - new StringDecoder()), - new CodecHttpMessageConverter(new Jaxb2Encoder(), - new Jaxb2Decoder()), + + List> converters = Arrays.asList( + new CodecHttpMessageConverter(new ByteBufferEncoder(), new ByteBufferDecoder()), + new CodecHttpMessageConverter(new StringEncoder(), new StringDecoder()), + new CodecHttpMessageConverter(new Jaxb2Encoder(), new Jaxb2Decoder()), new CodecHttpMessageConverter(new JacksonJsonEncoder(), new JacksonJsonDecoder(new JsonObjectDecoder()))); + // Annotation-based argument resolution + ConversionService cs = getConversionService(); + this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), false)); + this.argumentResolvers.add(new RequestParamMapMethodArgumentResolver()); + this.argumentResolvers.add(new PathVariableMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new PathVariableMapMethodArgumentResolver()); + this.argumentResolvers.add(new RequestBodyArgumentResolver(converters, cs)); + this.argumentResolvers.add(new RequestHeaderMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new RequestHeaderMapMethodArgumentResolver()); + this.argumentResolvers.add(new CookieValueMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new ExpressionValueMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new SessionAttributeMethodArgumentResolver(cs, getBeanFactory())); + this.argumentResolvers.add(new RequestAttributeMethodArgumentResolver(cs , getBeanFactory())); - this.argumentResolvers.add(new RequestParamArgumentResolver()); - this.argumentResolvers.add(new RequestBodyArgumentResolver(messageConverters, - this.conversionService)); + // Type-based argument resolution this.argumentResolvers.add(new ModelArgumentResolver()); + + // Catch-all + this.argumentResolvers.add(new RequestParamMethodArgumentResolver(cs, getBeanFactory(), true)); } } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java deleted file mode 100644 index b518e3f628..0000000000 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamArgumentResolver.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2002-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.web.reactive.result.method.annotation; - -import reactor.core.publisher.Mono; - -import org.springframework.core.MethodParameter; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - - -/** - * Support {@code @RequestParam} but for query params only. - * - * @author Rossen Stoyanchev - */ -public class RequestParamArgumentResolver implements HandlerMethodArgumentResolver { - - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(RequestParam.class); - } - - - @Override - public Mono resolveArgument(MethodParameter param, ModelMap model, ServerWebExchange exchange) { - RequestParam annotation = param.getParameterAnnotation(RequestParam.class); - String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName()); - UriComponents uriComponents = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build(); - return Mono.justOrEmpty(uriComponents.getQueryParams().getFirst(name)); - } - -} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java new file mode 100644 index 0000000000..50df6d3c4a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolver.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * Resolver for {@link Map} method arguments annotated with + * {@link RequestParam @RequestParam} where the annotation does not specify a + * request parameter name. See {@link RequestParamMethodArgumentResolver} for + * resolving {@link Map} method arguments with a request parameter name. + * + *

The created {@link Map} contains all request parameter name-value pairs. + * If the method parameter type is {@link MultiValueMap} instead, the created + * map contains all request parameters and all there values for cases where + * request parameters have multiple values. + * + * @author Rossen Stoyanchev + * @see RequestParamMethodArgumentResolver + */ +public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); + if (requestParam != null) { + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + return !StringUtils.hasText(requestParam.name()); + } + } + return false; + } + + @Override + public Mono resolveArgument(MethodParameter parameter, ModelMap model, ServerWebExchange exchange) { + Class paramType = parameter.getParameterType(); + MultiValueMap queryParams = exchange.getRequest().getQueryParams(); + if (MultiValueMap.class.isAssignableFrom(paramType)) { + return Mono.just(queryParams); + } + else { + return Mono.just(queryParams.toSingleValueMap()); + } + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java new file mode 100644 index 0000000000..b55bb3c1a4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolver.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.util.List; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolver for method arguments annotated with @{@link RequestParam}. + * + *

This resolver can also be created in default resolution mode in which + * simple types (int, long, etc.) not annotated with @{@link RequestParam} are + * also treated as request parameters with the parameter name derived from the + * argument name. + * + *

If the method parameter type is {@link Map}, the name specified in the + * annotation is used to resolve the request parameter String value. The value is + * then converted to a {@link Map} via type conversion assuming a suitable + * {@link Converter} has been registered. Or if a request parameter name is not + * specified the {@link RequestParamMapMethodArgumentResolver} is used instead + * to provide access to all request parameters in the form of a map. + * + * @author Rossen Stoyanchev + * @see RequestParamMapMethodArgumentResolver + */ +public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + private final boolean useDefaultResolution; + + + /** + * @param conversionService for type conversion (to be replaced with WebDataBinder) + * @param beanFactory a bean factory used for resolving ${...} placeholder + * and #{...} SpEL expressions in default values, or {@code null} if default + * values are not expected to contain expressions + * @param useDefaultResolution in default resolution mode a method argument + * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty}, + * is treated as a request parameter even if it isn't annotated, the + * request parameter name is derived from the method parameter name. + */ + public RequestParamMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) { + + super(conversionService, beanFactory); + this.useDefaultResolution = useDefaultResolution; + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + String paramName = parameter.getParameterAnnotation(RequestParam.class).name(); + return StringUtils.hasText(paramName); + } + else { + return true; + } + } + return (this.useDefaultResolution && BeanUtils.isSimpleProperty(parameter.getNestedParameterType())); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); + return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange) { + List paramValues = exchange.getRequest().getQueryParams().get(name); + Object result = null; + if (paramValues != null) { + result = (paramValues.size() == 1 ? paramValues.get(0) : paramValues); + } + return Mono.justOrEmpty(result); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Required " + type + " parameter '" + name + "' is not present"; + throw new ServerWebInputException(reason, parameter); + } + + + private static class RequestParamNamedValueInfo extends NamedValueInfo { + + public RequestParamNamedValueInfo() { + super("", false, ValueConstants.DEFAULT_NONE); + } + + public RequestParamNamedValueInfo(RequestParam annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java new file mode 100644 index 0000000000..b6eded3b22 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolver.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.util.Optional; + +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +/** + * Resolves method arguments annotated with an @{@link SessionAttribute}. + * + * @author Rossen Stoyanchev + * @see RequestAttributeMethodArgumentResolver + */ +public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + + public SessionAttributeMethodArgumentResolver(ConversionService conversionService, + ConfigurableBeanFactory beanFactory) { + + super(conversionService, beanFactory); + } + + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(SessionAttribute.class); + } + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + SessionAttribute annot = parameter.getParameterAnnotation(SessionAttribute.class); + return new NamedValueInfo(annot.name(), annot.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Mono resolveName(String name, MethodParameter parameter, ServerWebExchange exchange){ + return exchange.getSession().map(session -> session.getAttribute(name)) + .where(Optional::isPresent).map(Optional::get); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) { + String type = parameter.getNestedParameterType().getSimpleName(); + String reason = "Missing session attribute '" + name + "' of type " + type; + throw new ServerWebInputException(reason, parameter); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java new file mode 100644 index 0000000000..9289c3c459 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/server/ServerErrorException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.server; + +import java.util.Optional; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; + +/** + * Exception for errors that fit response status 500 (bad request) for use in + * Spring Web applications. The exception provides additional fields (e.g. + * an optional {@link MethodParameter} if related to the error). + * + * @author Rossen Stoyanchev + */ +public class ServerErrorException extends ResponseStatusException { + + private final MethodParameter parameter; + + + /** + * Constructor with an explanation only. + */ + public ServerErrorException(String reason) { + this(reason, null); + } + + /** + * Constructor for a 500 error linked to a specific {@code MethodParameter}. + */ + public ServerErrorException(String reason, MethodParameter parameter) { + this(reason, parameter, null); + } + + /** + * Constructor for a 500 error with a root cause. + */ + public ServerErrorException(String reason, MethodParameter parameter, Throwable cause) { + super(HttpStatus.INTERNAL_SERVER_ERROR, reason, cause); + this.parameter = parameter; + } + + + /** + * Return the {@code MethodParameter} associated with this error, if any. + */ + public Optional getMethodParameter() { + return Optional.ofNullable(this.parameter); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java index 3d9ae0bbef..939c67e217 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/InvocableHandlerMethodTests.java @@ -28,14 +28,16 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.Signal; import reactor.core.util.SignalKind; +import org.springframework.core.convert.support.GenericConversionService; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ModelMap; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.result.method.annotation.RequestParamArgumentResolver; +import org.springframework.web.reactive.result.method.annotation.RequestParamMethodArgumentResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.session.WebSessionManager; @@ -82,8 +84,10 @@ public class InvocableHandlerMethodTests { @Test public void resolveArgToZeroValues() throws Exception { when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/path")); + when(this.request.getQueryParams()).thenReturn(new LinkedMultiValueMap<>()); InvocableHandlerMethod hm = createHandlerMethod("singleArg", String.class); - hm.setHandlerMethodArgumentResolvers(Collections.singletonList(new RequestParamArgumentResolver())); + hm.setHandlerMethodArgumentResolvers(Collections.singletonList( + new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false))); Mono mono = hm.invokeForRequest(this.exchange, this.model); HandlerResult value = mono.get(); diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java new file mode 100644 index 0000000000..ae1b4b9f25 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestAttributesArgumentResolverTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Base class for {@code @RequestAttribute} and {@code @SessionAttribute} method + * argument resolution tests. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractRequestAttributesArgumentResolverTests { + + private HandlerMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private Method handleMethod; + + + @Before @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + this.resolver = createResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + WebSession webSession = mock(WebSession.class); + when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(webSession)); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), getHandleMethodName(), (Class[]) null); + } + + + protected abstract HandlerMethodArgumentResolver createResolver(); + + protected abstract String getHandleMethodName(); + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + this.exchange.getAttributes().put("specialFoo", foo); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithRequestAttribute( + @RequestAttribute Foo foo, + @RequestAttribute("specialFoo") Foo namedFoo, + @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, + @RequestAttribute(name="foo") Optional optionalFoo) { + } + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithSessionAttribute( + @SessionAttribute Foo foo, + @SessionAttribute("specialFoo") Foo namedFoo, + @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, + @SessionAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..e31b056e62 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/CookieValueMethodArgumentResolverTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Test fixture with {@link CookieValueMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class CookieValueMethodArgumentResolverTests { + + private CookieValueMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter cookieParameter; + private MethodParameter cookieStringParameter; + private MethodParameter stringParameter; + + + @Before + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new CookieValueMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", HttpCookie.class, String.class, String.class); + this.cookieParameter = new SynthesizingMethodParameter(method, 0); + this.cookieStringParameter = new SynthesizingMethodParameter(method, 1); + this.stringParameter = new SynthesizingMethodParameter(method, 2); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.cookieParameter)); + assertTrue(this.resolver.supportsParameter(this.cookieStringParameter)); + assertFalse(this.resolver.supportsParameter(this.stringParameter)); + } + + @Test + public void resolveCookieArgument() { + HttpCookie expected = new HttpCookie("name", "foo"); + this.exchange.getRequest().getCookies().add(expected.getName(), expected); + + Mono mono = this.resolver.resolveArgument(this.cookieParameter, null, this.exchange); + assertEquals(expected, mono.get()); + } + + @Test + public void resolveCookieStringArgument() { + HttpCookie cookie = new HttpCookie("name", "foo"); + this.exchange.getRequest().getCookies().add(cookie.getName(), cookie); + + Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); + assertEquals("Invalid result", cookie.getValue(), mono.get()); + } + + @Test + public void resolveCookieDefaultValue() { + Mono mono = this.resolver.resolveArgument(this.cookieStringParameter, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("bar", result); + } + + @Test + public void notFound() { + Mono mono = resolver.resolveArgument(this.cookieParameter, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + + @SuppressWarnings("unused") + public void params( + @CookieValue("name") HttpCookie cookie, + @CookieValue(name = "name", defaultValue = "bar") String cookieString, + String stringParam) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java new file mode 100644 index 0000000000..52ae4a2a11 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/ExpressionValueMethodArgumentResolverTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ExpressionValueMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class ExpressionValueMethodArgumentResolverTests { + + private ExpressionValueMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramSystemProperty; + private MethodParameter paramNotSupported; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new GenericConversionService(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + this.resolver = new ExpressionValueMethodArgumentResolver(conversionService, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", int.class, String.class); + this.paramSystemProperty = new MethodParameter(method, 0); + this.paramNotSupported = new MethodParameter(method, 1); + } + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(this.paramSystemProperty)); + assertFalse(this.resolver.supportsParameter(this.paramNotSupported)); + } + + @Test + public void resolveSystemProperty() throws Exception { + System.setProperty("systemProperty", "22"); + try { + Mono mono = this.resolver.resolveArgument(this.paramSystemProperty, null, this.exchange); + Object value = mono.get(); + assertEquals(22, value); + } + finally { + System.clearProperty("systemProperty"); + } + + } + + // TODO: test with expression for ServerWebExchange + + + @SuppressWarnings("unused") + public void params(@Value("#{systemProperties.systemProperty}") int param1, + String notSupported) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..851e77e804 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMapMethodArgumentResolverTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PathVariableMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class PathVariableMapMethodArgumentResolverTests { + + private PathVariableMapMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramMap; + private MethodParameter paramNamedMap; + private MethodParameter paramMapNoAnnot; + + + @Before + public void setUp() throws Exception { + this.resolver = new PathVariableMapMethodArgumentResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("handle", Map.class, Map.class, Map.class); + this.paramMap = new MethodParameter(method, 0); + this.paramNamedMap = new MethodParameter(method, 1); + this.paramMapNoAnnot = new MethodParameter(method, 2); + } + + @Test + public void supportsParameter() { + assertTrue(resolver.supportsParameter(paramMap)); + assertFalse(resolver.supportsParameter(paramNamedMap)); + assertFalse(resolver.supportsParameter(paramMapNoAnnot)); + } + + @Test + public void resolveArgument() throws Exception { + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("name1", "value1"); + uriTemplateVars.put("name2", "value2"); + this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertEquals(uriTemplateVars, result); + } + + @Test + public void resolveArgumentNoUriVars() throws Exception { + Mono mono = this.resolver.resolveArgument(this.paramMap, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertEquals(Collections.emptyMap(), result); + } + + + @SuppressWarnings("unused") + public void handle( + @PathVariable Map map, + @PathVariable(value = "name") Map namedMap, + Map mapWithoutAnnotat) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java new file mode 100644 index 0000000000..f79d713013 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolverTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PathVariableMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class PathVariableMethodArgumentResolverTests { + + private PathVariableMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramNamedString; + private MethodParameter paramString; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new DefaultConversionService(); + this.resolver = new PathVariableMethodArgumentResolver(conversionService, null); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("handle", String.class, String.class); + this.paramNamedString = new MethodParameter(method, 0); + this.paramString = new MethodParameter(method, 1); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.paramNamedString)); + assertFalse(this.resolver.supportsParameter(this.paramString)); + } + + @Test + public void resolveArgument() throws Exception { + Map uriTemplateVars = new HashMap<>(); + uriTemplateVars.put("name", "value"); + this.exchange.getAttributes().put(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVars); + + Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("value", result); + } + + @Test + public void handleMissingValue() throws Exception { + Mono mono = this.resolver.resolveArgument(this.paramNamedString, new ModelMap(), this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerErrorException.class); + } + + @SuppressWarnings("unused") + public void handle(@PathVariable(value = "name") String param1, String param2) { + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java new file mode 100644 index 0000000000..3d82d8e04e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +/** + * Unit tests for {@link RequestAttributeMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class RequestAttributeMethodArgumentResolverTests { + + private RequestAttributeMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private Method handleMethod; + + + @Before + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new RequestAttributeMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithRequestAttribute", (Class[]) null); + } + + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + this.exchange.getAttributes().put("specialFoo", foo); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + this.exchange.getAttributes().put("foo", foo); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithRequestAttribute( + @RequestAttribute Foo foo, + @RequestAttribute("specialFoo") Foo namedFoo, + @RequestAttribute(name="foo", required = false) Foo notRequiredFoo, + @RequestAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..a2e48579af --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolverTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestHeaderMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestHeaderMapMethodArgumentResolverTests { + + private RequestHeaderMapMethodArgumentResolver resolver; + + private MethodParameter paramMap; + private MethodParameter paramMultiValueMap; + private MethodParameter paramHttpHeaders; + private MethodParameter paramUnsupported; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + resolver = new RequestHeaderMapMethodArgumentResolver(); + + Method method = getClass().getMethod("params", Map.class, MultiValueMap.class, HttpHeaders.class, Map.class); + paramMap = new SynthesizingMethodParameter(method, 0); + paramMultiValueMap = new SynthesizingMethodParameter(method, 1); + paramHttpHeaders = new SynthesizingMethodParameter(method, 2); + paramUnsupported = new SynthesizingMethodParameter(method, 3); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void supportsParameter() { + assertTrue("Map parameter not supported", resolver.supportsParameter(paramMap)); + assertTrue("MultiValueMap parameter not supported", resolver.supportsParameter(paramMultiValueMap)); + assertTrue("HttpHeaders parameter not supported", resolver.supportsParameter(paramHttpHeaders)); + assertFalse("non-@RequestParam map supported", resolver.supportsParameter(paramUnsupported)); + } + + @Test + public void resolveMapArgument() throws Exception { + String name = "foo"; + String value = "bar"; + Map expected = Collections.singletonMap(name, value); + this.exchange.getRequest().getHeaders().add(name, value); + + Mono mono = this.resolver.resolveArgument(paramMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Map); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveMultiValueMapArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + + this.exchange.getRequest().getHeaders().add(name, value1); + this.exchange.getRequest().getHeaders().add(name, value2); + + MultiValueMap expected = new LinkedMultiValueMap<>(1); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(paramMultiValueMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof MultiValueMap); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveHttpHeadersArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + + this.exchange.getRequest().getHeaders().add(name, value1); + this.exchange.getRequest().getHeaders().add(name, value2); + + HttpHeaders expected = new HttpHeaders(); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(paramHttpHeaders, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof HttpHeaders); + assertEquals("Invalid result", expected, result); + } + + + @SuppressWarnings("unused") + public void params(@RequestHeader Map param1, + @RequestHeader MultiValueMap param2, + @RequestHeader HttpHeaders param3, + Map unsupported) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java new file mode 100644 index 0000000000..d8dcfbcece --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestHeaderMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestHeaderMethodArgumentResolverTests { + + private RequestHeaderMethodArgumentResolver resolver; + + private MethodParameter paramNamedDefaultValueStringHeader; + private MethodParameter paramNamedValueStringArray; + private MethodParameter paramSystemProperty; + private MethodParameter paramResolvedNameWithExpression; + private MethodParameter paramResolvedNameWithPlaceholder; + private MethodParameter paramNamedValueMap; + private MethodParameter paramDate; + private MethodParameter paramInstant; + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + ConversionService conversionService = new DefaultFormattingConversionService(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + this.resolver = new RequestHeaderMethodArgumentResolver(conversionService, context.getBeanFactory()); + + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + Method method = ReflectionUtils.findMethod(getClass(), "params", null); + this.paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 0); + this.paramNamedValueStringArray = new SynthesizingMethodParameter(method, 1); + this.paramSystemProperty = new SynthesizingMethodParameter(method, 2); + this.paramResolvedNameWithExpression = new SynthesizingMethodParameter(method, 3); + this.paramResolvedNameWithPlaceholder = new SynthesizingMethodParameter(method, 4); + this.paramNamedValueMap = new SynthesizingMethodParameter(method, 5); + this.paramDate = new SynthesizingMethodParameter(method, 6); + this.paramInstant = new SynthesizingMethodParameter(method, 7); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void supportsParameter() { + assertTrue("String parameter not supported", resolver.supportsParameter(paramNamedDefaultValueStringHeader)); + assertTrue("String array parameter not supported", resolver.supportsParameter(paramNamedValueStringArray)); + assertFalse("non-@RequestParam parameter supported", resolver.supportsParameter(paramNamedValueMap)); + } + + @Test + public void resolveStringArgument() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("name", expected); + + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + + @Test + public void resolveStringArrayArgument() throws Exception { + String[] expected = new String[] {"foo", "bar"}; + this.exchange.getRequest().getHeaders().put("name", Arrays.asList(expected)); + + Mono mono = this.resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String[]); + assertArrayEquals(expected, (String[]) result); + } + + @Test + public void resolveDefaultValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueStringHeader, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals("bar", result); + } + + @Test + public void resolveDefaultValueFromSystemProperty() throws Exception { + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramSystemProperty, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals("bar", result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemPropertyThroughExpression() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramResolvedNameWithExpression, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void resolveNameFromSystemPropertyThroughPlaceholder() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getHeaders().add("bar", expected); + + System.setProperty("systemProperty", "bar"); + try { + Mono mono = this.resolver.resolveArgument(paramResolvedNameWithPlaceholder, null, this.exchange); + Object result = mono.get(); + assertTrue(result instanceof String); + assertEquals(expected, result); + } + finally { + System.clearProperty("systemProperty"); + } + } + + @Test + public void notFound() throws Exception { + Mono mono = resolver.resolveArgument(paramNamedValueStringArray, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + @Test + public void dateConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + this.exchange.getRequest().getHeaders().add("name", rfc1123val); + + Mono mono = this.resolver.resolveArgument(paramDate, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Date); + assertEquals(new Date(rfc1123val), result); + } + + @Test + public void instantConversion() throws Exception { + String rfc1123val = "Thu, 21 Apr 2016 17:11:08 +0100"; + this.exchange.getRequest().getHeaders().add("name", rfc1123val); + + Mono mono = this.resolver.resolveArgument(paramInstant, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof Instant); + assertEquals(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val)), result); + } + + + @SuppressWarnings("unused") + public void params( + @RequestHeader(name = "name", defaultValue = "bar") String param1, + @RequestHeader("name") String[] param2, + @RequestHeader(name = "name", defaultValue="#{systemProperties.systemProperty}") String param3, + @RequestHeader("#{systemProperties.systemProperty}") String param4, + @RequestHeader("${systemProperty}") String param5, + @RequestHeader("name") Map unsupported, + @RequestHeader("name") Date dateParam, + @RequestHeader("name") Instant instantParam) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..c33daa06ef --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMapMethodArgumentResolverTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestParamMapMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestParamMapMethodArgumentResolverTests { + + private RequestParamMapMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramMap; + private MethodParameter paramMultiValueMap; + private MethodParameter paramNamedMap; + private MethodParameter paramMapWithoutAnnot; + + + + @Before + public void setUp() throws Exception { + this.resolver = new RequestParamMapMethodArgumentResolver(); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + Method method = getClass().getMethod("params", Map.class, MultiValueMap.class, Map.class, Map.class); + this.paramMap = new SynthesizingMethodParameter(method, 0); + this.paramMultiValueMap = new SynthesizingMethodParameter(method, 1); + this.paramNamedMap = new SynthesizingMethodParameter(method, 2); + this.paramMapWithoutAnnot = new SynthesizingMethodParameter(method, 3); + } + + + @Test + public void supportsParameter() { + assertTrue(this.resolver.supportsParameter(this.paramMap)); + assertTrue(this.resolver.supportsParameter(this.paramMultiValueMap)); + assertFalse(this.resolver.supportsParameter(this.paramNamedMap)); + assertFalse(this.resolver.supportsParameter(this.paramMapWithoutAnnot)); + } + + @Test + public void resolveMapArgument() throws Exception { + String name = "foo"; + String value = "bar"; + this.exchange.getRequest().getQueryParams().set(name, value); + Map expected = Collections.singletonMap(name, value); + + Mono mono = resolver.resolveArgument(paramMap, null, exchange); + Object result = mono.get(); + + assertTrue(result instanceof Map); + assertEquals(expected, result); + } + + @Test + public void resolveMultiValueMapArgument() throws Exception { + String name = "foo"; + String value1 = "bar"; + String value2 = "baz"; + this.exchange.getRequest().getQueryParams().put(name, Arrays.asList(value1, value2)); + + MultiValueMap expected = new LinkedMultiValueMap<>(1); + expected.add(name, value1); + expected.add(name, value2); + + Mono mono = this.resolver.resolveArgument(this.paramMultiValueMap, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof MultiValueMap); + assertEquals(expected, result); + } + + + @SuppressWarnings("unused") + public void params(@RequestParam Map param1, + @RequestParam MultiValueMap param2, + @RequestParam("name") Map param3, + Map param4) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java new file mode 100644 index 0000000000..4e6979dfcc --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestParamMethodArgumentResolverTests.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestParamMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class RequestParamMethodArgumentResolverTests { + + private RequestParamMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private MethodParameter paramNamedDefaultValueString; + private MethodParameter paramNamedStringArray; + private MethodParameter paramNamedMap; + private MethodParameter paramMap; + private MethodParameter paramStringNotAnnot; + private MethodParameter paramRequired; + private MethodParameter paramNotRequired; + private MethodParameter paramOptional; + + + @Before @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + ConversionService conversionService = new DefaultConversionService(); + this.resolver = new RequestParamMethodArgumentResolver(conversionService, null, true); + + ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + Method method = ReflectionUtils.findMethod(getClass(), "handle", (Class[]) null); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.paramNamedDefaultValueString = new SynthesizingMethodParameter(method, 0); + this.paramNamedStringArray = new SynthesizingMethodParameter(method, 1); + this.paramNamedMap = new SynthesizingMethodParameter(method, 2); + this.paramMap = new SynthesizingMethodParameter(method, 3); + this.paramStringNotAnnot = new SynthesizingMethodParameter(method, 4); + this.paramStringNotAnnot.initParameterNameDiscovery(paramNameDiscoverer); + this.paramRequired = new SynthesizingMethodParameter(method, 5); + this.paramNotRequired = new SynthesizingMethodParameter(method, 6); + this.paramOptional = new SynthesizingMethodParameter(method, 7); + } + + + @Test + public void supportsParameter() { + this.resolver = new RequestParamMethodArgumentResolver(new GenericConversionService(), null, true); + assertTrue(this.resolver.supportsParameter(this.paramNamedDefaultValueString)); + assertTrue(this.resolver.supportsParameter(this.paramNamedStringArray)); + assertTrue(this.resolver.supportsParameter(this.paramNamedMap)); + assertFalse(this.resolver.supportsParameter(this.paramMap)); + assertTrue(this.resolver.supportsParameter(this.paramStringNotAnnot)); + assertTrue(this.resolver.supportsParameter(this.paramRequired)); + assertTrue(this.resolver.supportsParameter(this.paramNotRequired)); + assertTrue(this.resolver.supportsParameter(this.paramOptional)); + + this.resolver = new RequestParamMethodArgumentResolver(new GenericConversionService(), null, false); + assertFalse(this.resolver.supportsParameter(this.paramStringNotAnnot)); + } + + @Test + public void resolveString() throws Exception { + String expected = "foo"; + this.exchange.getRequest().getQueryParams().set("name", expected); + + Mono mono = this.resolver.resolveArgument(this.paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("Invalid result", expected, result); + } + + @Test + public void resolveStringArray() throws Exception { + String[] expected = {"foo", "bar"}; + this.exchange.getRequest().getQueryParams().put("name", Arrays.asList(expected)); + + Mono mono = this.resolver.resolveArgument(this.paramNamedStringArray, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String[]); + assertArrayEquals(expected, (String[]) result); + } + + @Test + public void resolveDefaultValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("Invalid result", "bar", result); + } + + @Test + public void missingRequestParam() throws Exception { + Mono mono = this.resolver.resolveArgument(paramNamedStringArray, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + } + + @Test + public void resolveSimpleTypeParam() throws Exception { + this.exchange.getRequest().getQueryParams().set("stringNotAnnot", "plainValue"); + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertTrue(result instanceof String); + assertEquals("plainValue", result); + } + + @Test // SPR-8561 + public void resolveSimpleTypeParamToNull() throws Exception { + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertNull(result); + } + + @Test // SPR-10180 + public void resolveEmptyValueToDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("name", ""); + Mono mono = this.resolver.resolveArgument(paramNamedDefaultValueString, null, this.exchange); + Object result = mono.get(); + + assertEquals("bar", result); + } + + @Test + public void resolveEmptyValueWithoutDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("stringNotAnnot", ""); + Mono mono = this.resolver.resolveArgument(paramStringNotAnnot, null, this.exchange); + Object result = mono.get(); + + assertEquals("", result); + } + + @Test + public void resolveEmptyValueRequiredWithoutDefault() throws Exception { + this.exchange.getRequest().getQueryParams().set("name", ""); + Mono mono = this.resolver.resolveArgument(paramRequired, null, this.exchange); + Object result = mono.get(); + + assertEquals("", result); + } + + @Test + public void resolveOptionalParamValue() throws Exception { + Mono mono = this.resolver.resolveArgument(paramOptional, null, this.exchange); + Object result = mono.get(); + + assertEquals(Optional.empty(), result); + + this.exchange.getRequest().getQueryParams().set("name", "123"); + mono = resolver.resolveArgument(paramOptional, null, this.exchange); + result = mono.get(); + + assertEquals(Optional.class, result.getClass()); + Optional value = (Optional) result; + assertTrue(value.isPresent()); + assertEquals(123, value.get()); + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + public void handle( + @RequestParam(name = "name", defaultValue = "bar") String param1, + @RequestParam("name") String[] param2, + @RequestParam("name") Map param3, + @RequestParam Map param4, + String stringNotAnnot, + @RequestParam("name") String paramRequired, + @RequestParam(name = "name", required = false) String paramNotRequired, + @RequestParam("name") Optional paramOptional) { + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java new file mode 100644 index 0000000000..1bdbc9ce4a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/annotation/SessionAttributeMethodArgumentResolverTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.reactive.result.method.annotation; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.SessionAttribute; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link SessionAttributeMethodArgumentResolver}. + * @author Rossen Stoyanchev + */ +public class SessionAttributeMethodArgumentResolverTests { + + private SessionAttributeMethodArgumentResolver resolver; + + private ServerWebExchange exchange; + + private WebSession session; + + private Method handleMethod; + + + @Before + @SuppressWarnings("ConfusingArgumentToVarargsMethod") + public void setUp() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + ConversionService cs = new DefaultConversionService(); + this.resolver = new SessionAttributeMethodArgumentResolver(cs, context.getBeanFactory()); + + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + this.session = mock(WebSession.class); + when(sessionManager.getSession(this.exchange)).thenReturn(Mono.just(this.session)); + when(this.session.getAttribute(any())).thenReturn(Optional.empty()); + + this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class[]) null); + } + + @Test + public void supportsParameter() throws Exception { + assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0))); + assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4))); + } + + @Test + public void resolve() throws Exception { + MethodParameter param = initMethodParameter(0); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + TestSubscriber subscriber = new TestSubscriber<>(); + mono.subscribeWith(subscriber); + subscriber.assertError(ServerWebInputException.class); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveWithName() throws Exception { + MethodParameter param = initMethodParameter(1); + Foo foo = new Foo(); + when(this.session.getAttribute("specialFoo")).thenReturn(Optional.of(foo)); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveNotRequired() throws Exception { + MethodParameter param = initMethodParameter(2); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNull(mono.get()); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + assertSame(foo, mono.get()); + } + + @Test + public void resolveOptional() throws Exception { + MethodParameter param = initMethodParameter(3); + Mono mono = this.resolver.resolveArgument(param, null, this.exchange); + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + assertFalse(((Optional) mono.get()).isPresent()); + + Foo foo = new Foo(); + when(this.session.getAttribute("foo")).thenReturn(Optional.of(foo)); + mono = this.resolver.resolveArgument(param, null, this.exchange); + + assertNotNull(mono.get()); + assertEquals(Optional.class, mono.get().getClass()); + Optional optional = (Optional) mono.get(); + assertTrue(optional.isPresent()); + assertSame(foo, optional.get()); + } + + + private MethodParameter initMethodParameter(int parameterIndex) { + MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex); + param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + GenericTypeResolver.resolveParameterType(param, this.resolver.getClass()); + return param; + } + + + @SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"}) + private void handleWithSessionAttribute( + @SessionAttribute Foo foo, + @SessionAttribute("specialFoo") Foo namedFoo, + @SessionAttribute(name="foo", required = false) Foo notRequiredFoo, + @SessionAttribute(name="foo") Optional optionalFoo) { + } + + private static class Foo { + } + +}