001/*
002 * Copyright 2002-2019 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.method.annotation;
018
019import java.beans.PropertyEditor;
020import java.util.Collection;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024
025import javax.servlet.http.HttpServletRequest;
026import javax.servlet.http.Part;
027
028import org.springframework.beans.BeanUtils;
029import org.springframework.beans.factory.config.ConfigurableBeanFactory;
030import org.springframework.core.MethodParameter;
031import org.springframework.core.convert.ConversionService;
032import org.springframework.core.convert.TypeDescriptor;
033import org.springframework.core.convert.converter.Converter;
034import org.springframework.lang.Nullable;
035import org.springframework.util.Assert;
036import org.springframework.util.StringUtils;
037import org.springframework.web.bind.MissingServletRequestParameterException;
038import org.springframework.web.bind.WebDataBinder;
039import org.springframework.web.bind.annotation.RequestParam;
040import org.springframework.web.bind.annotation.RequestPart;
041import org.springframework.web.bind.annotation.ValueConstants;
042import org.springframework.web.context.request.NativeWebRequest;
043import org.springframework.web.method.support.UriComponentsContributor;
044import org.springframework.web.multipart.MultipartException;
045import org.springframework.web.multipart.MultipartFile;
046import org.springframework.web.multipart.MultipartRequest;
047import org.springframework.web.multipart.MultipartResolver;
048import org.springframework.web.multipart.support.MissingServletRequestPartException;
049import org.springframework.web.multipart.support.MultipartResolutionDelegate;
050import org.springframework.web.util.UriComponentsBuilder;
051
052/**
053 * Resolves method arguments annotated with @{@link RequestParam}, arguments of
054 * type {@link MultipartFile} in conjunction with Spring's {@link MultipartResolver}
055 * abstraction, and arguments of type {@code javax.servlet.http.Part} in conjunction
056 * with Servlet 3.0 multipart requests. This resolver can also be created in default
057 * resolution mode in which simple types (int, long, etc.) not annotated with
058 * {@link RequestParam @RequestParam} are also treated as request parameters with
059 * the parameter name derived from the argument name.
060 *
061 * <p>If the method parameter type is {@link Map}, the name specified in the
062 * annotation is used to resolve the request parameter String value. The value is
063 * then converted to a {@link Map} via type conversion assuming a suitable
064 * {@link Converter} or {@link PropertyEditor} has been registered.
065 * Or if a request parameter name is not specified the
066 * {@link RequestParamMapMethodArgumentResolver} is used instead to provide
067 * access to all request parameters in the form of a map.
068 *
069 * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved request
070 * header values that don't yet match the method parameter type.
071 *
072 * @author Arjen Poutsma
073 * @author Rossen Stoyanchev
074 * @author Brian Clozel
075 * @since 3.1
076 * @see RequestParamMapMethodArgumentResolver
077 */
078public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
079                implements UriComponentsContributor {
080
081        private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
082
083        private final boolean useDefaultResolution;
084
085
086        /**
087         * Create a new {@link RequestParamMethodArgumentResolver} instance.
088         * @param useDefaultResolution in default resolution mode a method argument
089         * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
090         * is treated as a request parameter even if it isn't annotated, the
091         * request parameter name is derived from the method parameter name.
092         */
093        public RequestParamMethodArgumentResolver(boolean useDefaultResolution) {
094                this.useDefaultResolution = useDefaultResolution;
095        }
096
097        /**
098         * Create a new {@link RequestParamMethodArgumentResolver} instance.
099         * @param beanFactory a bean factory used for resolving  ${...} placeholder
100         * and #{...} SpEL expressions in default values, or {@code null} if default
101         * values are not expected to contain expressions
102         * @param useDefaultResolution in default resolution mode a method argument
103         * that is a simple type, as defined in {@link BeanUtils#isSimpleProperty},
104         * is treated as a request parameter even if it isn't annotated, the
105         * request parameter name is derived from the method parameter name.
106         */
107        public RequestParamMethodArgumentResolver(@Nullable ConfigurableBeanFactory beanFactory,
108                        boolean useDefaultResolution) {
109
110                super(beanFactory);
111                this.useDefaultResolution = useDefaultResolution;
112        }
113
114
115        /**
116         * Supports the following:
117         * <ul>
118         * <li>@RequestParam-annotated method arguments.
119         * This excludes {@link Map} params where the annotation does not specify a name.
120         * See {@link RequestParamMapMethodArgumentResolver} instead for such params.
121         * <li>Arguments of type {@link MultipartFile} unless annotated with @{@link RequestPart}.
122         * <li>Arguments of type {@code Part} unless annotated with @{@link RequestPart}.
123         * <li>In default resolution mode, simple type arguments even if not with @{@link RequestParam}.
124         * </ul>
125         */
126        @Override
127        public boolean supportsParameter(MethodParameter parameter) {
128                if (parameter.hasParameterAnnotation(RequestParam.class)) {
129                        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
130                                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
131                                return (requestParam != null && StringUtils.hasText(requestParam.name()));
132                        }
133                        else {
134                                return true;
135                        }
136                }
137                else {
138                        if (parameter.hasParameterAnnotation(RequestPart.class)) {
139                                return false;
140                        }
141                        parameter = parameter.nestedIfOptional();
142                        if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
143                                return true;
144                        }
145                        else if (this.useDefaultResolution) {
146                                return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
147                        }
148                        else {
149                                return false;
150                        }
151                }
152        }
153
154        @Override
155        protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
156                RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
157                return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
158        }
159
160        @Override
161        @Nullable
162        protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
163                HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
164
165                if (servletRequest != null) {
166                        Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
167                        if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
168                                return mpArg;
169                        }
170                }
171
172                Object arg = null;
173                MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
174                if (multipartRequest != null) {
175                        List<MultipartFile> files = multipartRequest.getFiles(name);
176                        if (!files.isEmpty()) {
177                                arg = (files.size() == 1 ? files.get(0) : files);
178                        }
179                }
180                if (arg == null) {
181                        String[] paramValues = request.getParameterValues(name);
182                        if (paramValues != null) {
183                                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
184                        }
185                }
186                return arg;
187        }
188
189        @Override
190        protected void handleMissingValue(String name, MethodParameter parameter, NativeWebRequest request)
191                        throws Exception {
192
193                HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
194                if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
195                        if (servletRequest == null || !MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
196                                throw new MultipartException("Current request is not a multipart request");
197                        }
198                        else {
199                                throw new MissingServletRequestPartException(name);
200                        }
201                }
202                else {
203                        throw new MissingServletRequestParameterException(name,
204                                        parameter.getNestedParameterType().getSimpleName());
205                }
206        }
207
208        @Override
209        public void contributeMethodArgument(MethodParameter parameter, @Nullable Object value,
210                        UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
211
212                Class<?> paramType = parameter.getNestedParameterType();
213                if (Map.class.isAssignableFrom(paramType) || MultipartFile.class == paramType || Part.class == paramType) {
214                        return;
215                }
216
217                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
218                String name = (requestParam != null && StringUtils.hasLength(requestParam.name()) ?
219                                requestParam.name() : parameter.getParameterName());
220                Assert.state(name != null, "Unresolvable parameter name");
221
222                parameter = parameter.nestedIfOptional();
223                if (value instanceof Optional) {
224                        value = ((Optional<?>) value).orElse(null);
225                }
226
227                if (value == null) {
228                        if (requestParam != null &&
229                                        (!requestParam.required() || !requestParam.defaultValue().equals(ValueConstants.DEFAULT_NONE))) {
230                                return;
231                        }
232                        builder.queryParam(name);
233                }
234                else if (value instanceof Collection) {
235                        for (Object element : (Collection<?>) value) {
236                                element = formatUriValue(conversionService, TypeDescriptor.nested(parameter, 1), element);
237                                builder.queryParam(name, element);
238                        }
239                }
240                else {
241                        builder.queryParam(name, formatUriValue(conversionService, new TypeDescriptor(parameter), value));
242                }
243        }
244
245        @Nullable
246        protected String formatUriValue(
247                        @Nullable ConversionService cs, @Nullable TypeDescriptor sourceType, @Nullable Object value) {
248
249                if (value == null) {
250                        return null;
251                }
252                else if (value instanceof String) {
253                        return (String) value;
254                }
255                else if (cs != null) {
256                        return (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR);
257                }
258                else {
259                        return value.toString();
260                }
261        }
262
263
264        private static class RequestParamNamedValueInfo extends NamedValueInfo {
265
266                public RequestParamNamedValueInfo() {
267                        super("", false, ValueConstants.DEFAULT_NONE);
268                }
269
270                public RequestParamNamedValueInfo(RequestParam annotation) {
271                        super(annotation.name(), annotation.required(), annotation.defaultValue());
272                }
273        }
274
275}