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