001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.reactive.result.method.annotation;
018
019import java.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import reactor.core.publisher.Mono;
023
024import org.springframework.beans.ConversionNotSupportedException;
025import org.springframework.beans.TypeMismatchException;
026import org.springframework.beans.factory.config.BeanExpressionContext;
027import org.springframework.beans.factory.config.BeanExpressionResolver;
028import org.springframework.beans.factory.config.ConfigurableBeanFactory;
029import org.springframework.core.MethodParameter;
030import org.springframework.core.ReactiveAdapterRegistry;
031import org.springframework.lang.Nullable;
032import org.springframework.ui.Model;
033import org.springframework.web.bind.WebDataBinder;
034import org.springframework.web.bind.annotation.ValueConstants;
035import org.springframework.web.reactive.BindingContext;
036import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
037import org.springframework.web.server.ServerErrorException;
038import org.springframework.web.server.ServerWebExchange;
039import org.springframework.web.server.ServerWebInputException;
040
041/**
042 * Abstract base class for resolving method arguments from a named value.
043 * Request parameters, request headers, and path variables are examples of named
044 * values. Each may have a name, a required flag, and a default value.
045 *
046 * <p>Subclasses define how to do the following:
047 * <ul>
048 * <li>Obtain named value information for a method parameter
049 * <li>Resolve names into argument values
050 * <li>Handle missing argument values when argument values are required
051 * <li>Optionally handle a resolved value
052 * </ul>
053 *
054 * <p>A default value string can contain ${...} placeholders and Spring Expression
055 * Language #{...} expressions. For this to work a
056 * {@link ConfigurableBeanFactory} must be supplied to the class constructor.
057 *
058 * @author Rossen Stoyanchev
059 * @since 5.0
060 */
061public abstract class AbstractNamedValueArgumentResolver extends HandlerMethodArgumentResolverSupport {
062
063        @Nullable
064        private final ConfigurableBeanFactory configurableBeanFactory;
065
066        @Nullable
067        private final BeanExpressionContext expressionContext;
068
069        private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
070
071
072        /**
073         * Create a new {@link AbstractNamedValueArgumentResolver} instance.
074         * @param factory a bean factory to use for resolving {@code ${...}} placeholder
075         * and {@code #{...}} SpEL expressions in default values, or {@code null} if default
076         * values are not expected to contain expressions
077         * @param registry for checking reactive type wrappers
078         */
079        public AbstractNamedValueArgumentResolver(@Nullable ConfigurableBeanFactory factory,
080                        ReactiveAdapterRegistry registry) {
081
082                super(registry);
083                this.configurableBeanFactory = factory;
084                this.expressionContext = (factory != null ? new BeanExpressionContext(factory, null) : null);
085        }
086
087
088        @Override
089        public Mono<Object> resolveArgument(
090                        MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
091
092                NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
093                MethodParameter nestedParameter = parameter.nestedIfOptional();
094
095                Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
096                if (resolvedName == null) {
097                        return Mono.error(new IllegalArgumentException(
098                                        "Specified name must not resolve to null: [" + namedValueInfo.name + "]"));
099                }
100
101                Model model = bindingContext.getModel();
102
103                return resolveName(resolvedName.toString(), nestedParameter, exchange)
104                                .flatMap(arg -> {
105                                        if ("".equals(arg) && namedValueInfo.defaultValue != null) {
106                                                arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
107                                        }
108                                        arg = applyConversion(arg, namedValueInfo, parameter, bindingContext, exchange);
109                                        handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange);
110                                        return Mono.justOrEmpty(arg);
111                                })
112                                .switchIfEmpty(getDefaultValue(
113                                                namedValueInfo, parameter, bindingContext, model, exchange));
114        }
115
116        /**
117         * Obtain the named value for the given method parameter.
118         */
119        private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
120                NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
121                if (namedValueInfo == null) {
122                        namedValueInfo = createNamedValueInfo(parameter);
123                        namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
124                        this.namedValueInfoCache.put(parameter, namedValueInfo);
125                }
126                return namedValueInfo;
127        }
128
129        /**
130         * Create the {@link NamedValueInfo} object for the given method parameter.
131         * Implementations typically retrieve the method annotation by means of
132         * {@link MethodParameter#getParameterAnnotation(Class)}.
133         * @param parameter the method parameter
134         * @return the named value information
135         */
136        protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
137
138        /**
139         * Create a new NamedValueInfo based on the given NamedValueInfo with
140         * sanitized values.
141         */
142        private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
143                String name = info.name;
144                if (info.name.isEmpty()) {
145                        name = parameter.getParameterName();
146                        if (name == null) {
147                                throw new IllegalArgumentException(
148                                                "Name for argument of type [" + parameter.getNestedParameterType().getName() +
149                                                "] not specified, and parameter name information not found in class file either.");
150                        }
151                }
152                String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
153                return new NamedValueInfo(name, info.required, defaultValue);
154        }
155
156        /**
157         * Resolve the given annotation-specified value,
158         * potentially containing placeholders and expressions.
159         */
160        @Nullable
161        private Object resolveEmbeddedValuesAndExpressions(String value) {
162                if (this.configurableBeanFactory == null || this.expressionContext == null) {
163                        return value;
164                }
165                String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
166                BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
167                if (exprResolver == null) {
168                        return value;
169                }
170                return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
171        }
172
173        /**
174         * Resolve the given parameter type and value name into an argument value.
175         * @param name the name of the value being resolved
176         * @param parameter the method parameter to resolve to an argument value
177         * (pre-nested in case of a {@link java.util.Optional} declaration)
178         * @param exchange the current exchange
179         * @return the resolved argument (may be empty {@link Mono})
180         */
181        protected abstract Mono<Object> resolveName(String name, MethodParameter parameter, ServerWebExchange exchange);
182
183        /**
184         * Apply type conversion if necessary.
185         */
186        @Nullable
187        private Object applyConversion(@Nullable Object value, NamedValueInfo namedValueInfo, MethodParameter parameter,
188                        BindingContext bindingContext, ServerWebExchange exchange) {
189
190                WebDataBinder binder = bindingContext.createDataBinder(exchange, namedValueInfo.name);
191                try {
192                        value = binder.convertIfNecessary(value, parameter.getParameterType(), parameter);
193                }
194                catch (ConversionNotSupportedException ex) {
195                        throw new ServerErrorException("Conversion not supported.", parameter, ex);
196                }
197                catch (TypeMismatchException ex) {
198                        throw new ServerWebInputException("Type mismatch.", parameter, ex);
199                }
200                return value;
201        }
202
203        /**
204         * Resolve the default value, if any.
205         */
206        private Mono<Object> getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter,
207                        BindingContext bindingContext, Model model, ServerWebExchange exchange) {
208
209                return Mono.fromSupplier(() -> {
210                        Object value = null;
211                        if (namedValueInfo.defaultValue != null) {
212                                value = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
213                        }
214                        else if (namedValueInfo.required && !parameter.isOptional()) {
215                                handleMissingValue(namedValueInfo.name, parameter, exchange);
216                        }
217                        value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType());
218                        value = applyConversion(value, namedValueInfo, parameter, bindingContext, exchange);
219                        handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange);
220                        return value;
221                });
222        }
223
224        /**
225         * Invoked when a named value is required, but
226         * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned
227         * {@code null} and there is no default value. Subclasses typically throw an
228         * exception in this case.
229         * @param name the name for the value
230         * @param parameter the method parameter
231         * @param exchange the current exchange
232         */
233        @SuppressWarnings("UnusedParameters")
234        protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
235                handleMissingValue(name, parameter);
236        }
237
238        /**
239         * Invoked when a named value is required, but
240         * {@link #resolveName(String, MethodParameter, ServerWebExchange)} returned
241         * {@code null} and there is no default value. Subclasses typically throw an
242         * exception in this case.
243         * @param name the name for the value
244         * @param parameter the method parameter
245         */
246        protected void handleMissingValue(String name, MethodParameter parameter) {
247                String typeName = parameter.getNestedParameterType().getSimpleName();
248                throw new ServerWebInputException("Missing argument '" + name + "' for method " +
249                                "parameter of type " + typeName, parameter);
250        }
251
252        /**
253         * A {@code null} results in a {@code false} value for {@code boolean}s or
254         * an exception for other primitives.
255         */
256        @Nullable
257        private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
258                if (value == null) {
259                        if (Boolean.TYPE.equals(paramType)) {
260                                return Boolean.FALSE;
261                        }
262                        else if (paramType.isPrimitive()) {
263                                throw new IllegalStateException("Optional " + paramType.getSimpleName() +
264                                                " parameter '" + name + "' is present but cannot be translated into a" +
265                                                " null value due to being declared as a primitive type. " +
266                                                "Consider declaring it as object wrapper for the corresponding primitive type.");
267                        }
268                }
269                return value;
270        }
271
272        /**
273         * Invoked after a value is resolved.
274         * @param arg the resolved argument value
275         * @param name the argument name
276         * @param parameter the argument parameter type
277         * @param model the model
278         * @param exchange the current exchange
279         */
280        @SuppressWarnings("UnusedParameters")
281        protected void handleResolvedValue(
282                        @Nullable Object arg, String name, MethodParameter parameter, Model model, ServerWebExchange exchange) {
283        }
284
285
286        /**
287         * Represents the information about a named value, including name, whether
288         * it's required and a default value.
289         */
290        protected static class NamedValueInfo {
291
292                private final String name;
293
294                private final boolean required;
295
296                @Nullable
297                private final String defaultValue;
298
299                public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
300                        this.name = name;
301                        this.required = required;
302                        this.defaultValue = defaultValue;
303                }
304        }
305
306}