001/*
002 * Copyright 2002-2018 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.servlet.mvc.method.annotation;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.PushbackInputStream;
022import java.lang.annotation.Annotation;
023import java.lang.reflect.Type;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.EnumSet;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Optional;
031import java.util.Set;
032
033import javax.servlet.http.HttpServletRequest;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037
038import org.springframework.core.MethodParameter;
039import org.springframework.core.ResolvableType;
040import org.springframework.core.annotation.AnnotationUtils;
041import org.springframework.core.log.LogFormatUtils;
042import org.springframework.http.HttpHeaders;
043import org.springframework.http.HttpInputMessage;
044import org.springframework.http.HttpMethod;
045import org.springframework.http.HttpRequest;
046import org.springframework.http.InvalidMediaTypeException;
047import org.springframework.http.MediaType;
048import org.springframework.http.converter.GenericHttpMessageConverter;
049import org.springframework.http.converter.HttpMessageConverter;
050import org.springframework.http.converter.HttpMessageNotReadableException;
051import org.springframework.http.server.ServletServerHttpRequest;
052import org.springframework.lang.Nullable;
053import org.springframework.util.Assert;
054import org.springframework.util.StreamUtils;
055import org.springframework.validation.Errors;
056import org.springframework.validation.annotation.Validated;
057import org.springframework.web.HttpMediaTypeNotSupportedException;
058import org.springframework.web.bind.WebDataBinder;
059import org.springframework.web.context.request.NativeWebRequest;
060import org.springframework.web.method.support.HandlerMethodArgumentResolver;
061
062/**
063 * A base class for resolving method argument values by reading from the body of
064 * a request with {@link HttpMessageConverter HttpMessageConverters}.
065 *
066 * @author Arjen Poutsma
067 * @author Rossen Stoyanchev
068 * @author Juergen Hoeller
069 * @since 3.1
070 */
071public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
072
073        private static final Set<HttpMethod> SUPPORTED_METHODS =
074                        EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
075
076        private static final Object NO_VALUE = new Object();
077
078
079        protected final Log logger = LogFactory.getLog(getClass());
080
081        protected final List<HttpMessageConverter<?>> messageConverters;
082
083        protected final List<MediaType> allSupportedMediaTypes;
084
085        private final RequestResponseBodyAdviceChain advice;
086
087
088        /**
089         * Basic constructor with converters only.
090         */
091        public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
092                this(converters, null);
093        }
094
095        /**
096         * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}.
097         * @since 4.2
098         */
099        public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
100                        @Nullable List<Object> requestResponseBodyAdvice) {
101
102                Assert.notEmpty(converters, "'messageConverters' must not be empty");
103                this.messageConverters = converters;
104                this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
105                this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
106        }
107
108
109        /**
110         * Return the media types supported by all provided message converters sorted
111         * by specificity via {@link MediaType#sortBySpecificity(List)}.
112         */
113        private static List<MediaType> getAllSupportedMediaTypes(List<HttpMessageConverter<?>> messageConverters) {
114                Set<MediaType> allSupportedMediaTypes = new LinkedHashSet<>();
115                for (HttpMessageConverter<?> messageConverter : messageConverters) {
116                        allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
117                }
118                List<MediaType> result = new ArrayList<>(allSupportedMediaTypes);
119                MediaType.sortBySpecificity(result);
120                return Collections.unmodifiableList(result);
121        }
122
123
124        /**
125         * Return the configured {@link RequestBodyAdvice} and
126         * {@link RequestBodyAdvice} where each instance may be wrapped as a
127         * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
128         */
129        RequestResponseBodyAdviceChain getAdvice() {
130                return this.advice;
131        }
132
133        /**
134         * Create the method argument value of the expected parameter type by
135         * reading from the given request.
136         * @param <T> the expected type of the argument value to be created
137         * @param webRequest the current request
138         * @param parameter the method parameter descriptor (may be {@code null})
139         * @param paramType the type of the argument value to be created
140         * @return the created method argument value
141         * @throws IOException if the reading from the request fails
142         * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
143         */
144        @Nullable
145        protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
146                        Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
147
148                HttpInputMessage inputMessage = createInputMessage(webRequest);
149                return readWithMessageConverters(inputMessage, parameter, paramType);
150        }
151
152        /**
153         * Create the method argument value of the expected parameter type by reading
154         * from the given HttpInputMessage.
155         * @param <T> the expected type of the argument value to be created
156         * @param inputMessage the HTTP input message representing the current request
157         * @param parameter the method parameter descriptor
158         * @param targetType the target type, not necessarily the same as the method
159         * parameter type, e.g. for {@code HttpEntity<String>}.
160         * @return the created method argument value
161         * @throws IOException if the reading from the request fails
162         * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
163         */
164        @SuppressWarnings("unchecked")
165        @Nullable
166        protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
167                        Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
168
169                MediaType contentType;
170                boolean noContentType = false;
171                try {
172                        contentType = inputMessage.getHeaders().getContentType();
173                }
174                catch (InvalidMediaTypeException ex) {
175                        throw new HttpMediaTypeNotSupportedException(ex.getMessage());
176                }
177                if (contentType == null) {
178                        noContentType = true;
179                        contentType = MediaType.APPLICATION_OCTET_STREAM;
180                }
181
182                Class<?> contextClass = parameter.getContainingClass();
183                Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
184                if (targetClass == null) {
185                        ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
186                        targetClass = (Class<T>) resolvableType.resolve();
187                }
188
189                HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
190                Object body = NO_VALUE;
191
192                EmptyBodyCheckingHttpInputMessage message;
193                try {
194                        message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
195
196                        for (HttpMessageConverter<?> converter : this.messageConverters) {
197                                Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
198                                GenericHttpMessageConverter<?> genericConverter =
199                                                (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
200                                if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
201                                                (targetClass != null && converter.canRead(targetClass, contentType))) {
202                                        if (message.hasBody()) {
203                                                HttpInputMessage msgToUse =
204                                                                getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
205                                                body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
206                                                                ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
207                                                body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
208                                        }
209                                        else {
210                                                body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
211                                        }
212                                        break;
213                                }
214                        }
215                }
216                catch (IOException ex) {
217                        throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
218                }
219
220                if (body == NO_VALUE) {
221                        if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
222                                        (noContentType && !message.hasBody())) {
223                                return null;
224                        }
225                        throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
226                }
227
228                MediaType selectedContentType = contentType;
229                Object theBody = body;
230                LogFormatUtils.traceDebug(logger, traceOn -> {
231                        String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
232                        return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
233                });
234
235                return body;
236        }
237
238        /**
239         * Create a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
240         * @param webRequest the web request to create an input message from
241         * @return the input message
242         */
243        protected ServletServerHttpRequest createInputMessage(NativeWebRequest webRequest) {
244                HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
245                Assert.state(servletRequest != null, "No HttpServletRequest");
246                return new ServletServerHttpRequest(servletRequest);
247        }
248
249        /**
250         * Validate the binding target if applicable.
251         * <p>The default implementation checks for {@code @javax.validation.Valid},
252         * Spring's {@link org.springframework.validation.annotation.Validated},
253         * and custom annotations whose name starts with "Valid".
254         * @param binder the DataBinder to be used
255         * @param parameter the method parameter descriptor
256         * @since 4.1.5
257         * @see #isBindExceptionRequired
258         */
259        protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
260                Annotation[] annotations = parameter.getParameterAnnotations();
261                for (Annotation ann : annotations) {
262                        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
263                        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
264                                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
265                                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
266                                binder.validate(validationHints);
267                                break;
268                        }
269                }
270        }
271
272        /**
273         * Whether to raise a fatal bind exception on validation errors.
274         * @param binder the data binder used to perform data binding
275         * @param parameter the method parameter descriptor
276         * @return {@code true} if the next method argument is not of type {@link Errors}
277         * @since 4.1.5
278         */
279        protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
280                int i = parameter.getParameterIndex();
281                Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
282                boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
283                return !hasBindingResult;
284        }
285
286        /**
287         * Adapt the given argument against the method parameter, if necessary.
288         * @param arg the resolved argument
289         * @param parameter the method parameter descriptor
290         * @return the adapted argument, or the original resolved argument as-is
291         * @since 4.3.5
292         */
293        @Nullable
294        protected Object adaptArgumentIfNecessary(@Nullable Object arg, MethodParameter parameter) {
295                if (parameter.getParameterType() == Optional.class) {
296                        if (arg == null || (arg instanceof Collection && ((Collection<?>) arg).isEmpty()) ||
297                                        (arg instanceof Object[] && ((Object[]) arg).length == 0)) {
298                                return Optional.empty();
299                        }
300                        else {
301                                return Optional.of(arg);
302                        }
303                }
304                return arg;
305        }
306
307
308        private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
309
310                private final HttpHeaders headers;
311
312                @Nullable
313                private final InputStream body;
314
315                public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
316                        this.headers = inputMessage.getHeaders();
317                        InputStream inputStream = inputMessage.getBody();
318                        if (inputStream.markSupported()) {
319                                inputStream.mark(1);
320                                this.body = (inputStream.read() != -1 ? inputStream : null);
321                                inputStream.reset();
322                        }
323                        else {
324                                PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
325                                int b = pushbackInputStream.read();
326                                if (b == -1) {
327                                        this.body = null;
328                                }
329                                else {
330                                        this.body = pushbackInputStream;
331                                        pushbackInputStream.unread(b);
332                                }
333                        }
334                }
335
336                @Override
337                public HttpHeaders getHeaders() {
338                        return this.headers;
339                }
340
341                @Override
342                public InputStream getBody() {
343                        return (this.body != null ? this.body : StreamUtils.emptyInput());
344                }
345
346                public boolean hasBody() {
347                        return (this.body != null);
348                }
349        }
350
351}