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.lang.reflect.Type;
021import java.util.List;
022
023import javax.servlet.http.HttpServletRequest;
024
025import org.springframework.core.Conventions;
026import org.springframework.core.MethodParameter;
027import org.springframework.core.annotation.AnnotatedElementUtils;
028import org.springframework.http.converter.HttpMessageConverter;
029import org.springframework.http.converter.HttpMessageNotReadableException;
030import org.springframework.http.converter.HttpMessageNotWritableException;
031import org.springframework.http.server.ServletServerHttpRequest;
032import org.springframework.http.server.ServletServerHttpResponse;
033import org.springframework.lang.Nullable;
034import org.springframework.util.Assert;
035import org.springframework.validation.BindingResult;
036import org.springframework.web.HttpMediaTypeNotAcceptableException;
037import org.springframework.web.HttpMediaTypeNotSupportedException;
038import org.springframework.web.accept.ContentNegotiationManager;
039import org.springframework.web.bind.MethodArgumentNotValidException;
040import org.springframework.web.bind.WebDataBinder;
041import org.springframework.web.bind.annotation.RequestBody;
042import org.springframework.web.bind.annotation.ResponseBody;
043import org.springframework.web.bind.support.WebDataBinderFactory;
044import org.springframework.web.context.request.NativeWebRequest;
045import org.springframework.web.method.support.ModelAndViewContainer;
046import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
047
048/**
049 * Resolves method arguments annotated with {@code @RequestBody} and handles return
050 * values from methods annotated with {@code @ResponseBody} by reading and writing
051 * to the body of the request or response with an {@link HttpMessageConverter}.
052 *
053 * <p>An {@code @RequestBody} method argument is also validated if it is annotated
054 * with {@code @javax.validation.Valid}. In case of validation failure,
055 * {@link MethodArgumentNotValidException} is raised and results in an HTTP 400
056 * response status code if {@link DefaultHandlerExceptionResolver} is configured.
057 *
058 * @author Arjen Poutsma
059 * @author Rossen Stoyanchev
060 * @author Juergen Hoeller
061 * @since 3.1
062 */
063public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
064
065        /**
066         * Basic constructor with converters only. Suitable for resolving
067         * {@code @RequestBody}. For handling {@code @ResponseBody} consider also
068         * providing a {@code ContentNegotiationManager}.
069         */
070        public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
071                super(converters);
072        }
073
074        /**
075         * Basic constructor with converters and {@code ContentNegotiationManager}.
076         * Suitable for resolving {@code @RequestBody} and handling
077         * {@code @ResponseBody} without {@code Request~} or
078         * {@code ResponseBodyAdvice}.
079         */
080        public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
081                        @Nullable ContentNegotiationManager manager) {
082
083                super(converters, manager);
084        }
085
086        /**
087         * Complete constructor for resolving {@code @RequestBody} method arguments.
088         * For handling {@code @ResponseBody} consider also providing a
089         * {@code ContentNegotiationManager}.
090         * @since 4.2
091         */
092        public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
093                        @Nullable List<Object> requestResponseBodyAdvice) {
094
095                super(converters, null, requestResponseBodyAdvice);
096        }
097
098        /**
099         * Complete constructor for resolving {@code @RequestBody} and handling
100         * {@code @ResponseBody}.
101         */
102        public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
103                        @Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) {
104
105                super(converters, manager, requestResponseBodyAdvice);
106        }
107
108
109        @Override
110        public boolean supportsParameter(MethodParameter parameter) {
111                return parameter.hasParameterAnnotation(RequestBody.class);
112        }
113
114        @Override
115        public boolean supportsReturnType(MethodParameter returnType) {
116                return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
117                                returnType.hasMethodAnnotation(ResponseBody.class));
118        }
119
120        /**
121         * Throws MethodArgumentNotValidException if validation fails.
122         * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
123         * is {@code true} and there is no body content or if there is no suitable
124         * converter to read the content with.
125         */
126        @Override
127        public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
128                        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
129
130                parameter = parameter.nestedIfOptional();
131                Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
132                String name = Conventions.getVariableNameForParameter(parameter);
133
134                if (binderFactory != null) {
135                        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
136                        if (arg != null) {
137                                validateIfApplicable(binder, parameter);
138                                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
139                                        throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
140                                }
141                        }
142                        if (mavContainer != null) {
143                                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
144                        }
145                }
146
147                return adaptArgumentIfNecessary(arg, parameter);
148        }
149
150        @Override
151        protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
152                        Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
153
154                HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
155                Assert.state(servletRequest != null, "No HttpServletRequest");
156                ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
157
158                Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
159                if (arg == null && checkRequired(parameter)) {
160                        throw new HttpMessageNotReadableException("Required request body is missing: " +
161                                        parameter.getExecutable().toGenericString(), inputMessage);
162                }
163                return arg;
164        }
165
166        protected boolean checkRequired(MethodParameter parameter) {
167                RequestBody requestBody = parameter.getParameterAnnotation(RequestBody.class);
168                return (requestBody != null && requestBody.required() && !parameter.isOptional());
169        }
170
171        @Override
172        public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
173                        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
174                        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
175
176                mavContainer.setRequestHandled(true);
177                ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
178                ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
179
180                // Try even with null return value. ResponseBodyAdvice could get involved.
181                writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
182        }
183
184}