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.servlet.mvc.method.annotation;
018
019import java.io.IOException;
020import java.lang.reflect.ParameterizedType;
021import java.lang.reflect.Type;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpServletResponse;
029
030import org.springframework.core.MethodParameter;
031import org.springframework.core.ResolvableType;
032import org.springframework.http.HttpEntity;
033import org.springframework.http.HttpHeaders;
034import org.springframework.http.HttpMethod;
035import org.springframework.http.RequestEntity;
036import org.springframework.http.ResponseEntity;
037import org.springframework.http.converter.HttpMessageConverter;
038import org.springframework.http.server.ServletServerHttpRequest;
039import org.springframework.http.server.ServletServerHttpResponse;
040import org.springframework.lang.Nullable;
041import org.springframework.ui.ModelMap;
042import org.springframework.util.Assert;
043import org.springframework.util.CollectionUtils;
044import org.springframework.util.StringUtils;
045import org.springframework.web.HttpMediaTypeNotSupportedException;
046import org.springframework.web.accept.ContentNegotiationManager;
047import org.springframework.web.bind.support.WebDataBinderFactory;
048import org.springframework.web.context.request.NativeWebRequest;
049import org.springframework.web.context.request.ServletWebRequest;
050import org.springframework.web.method.support.ModelAndViewContainer;
051import org.springframework.web.servlet.mvc.support.RedirectAttributes;
052import org.springframework.web.servlet.support.RequestContextUtils;
053
054/**
055 * Resolves {@link HttpEntity} and {@link RequestEntity} method argument values
056 * and also handles {@link HttpEntity} and {@link ResponseEntity} return values.
057 *
058 * <p>An {@link HttpEntity} return type has a specific purpose. Therefore this
059 * handler should be configured ahead of handlers that support any return
060 * value type annotated with {@code @ModelAttribute} or {@code @ResponseBody}
061 * to ensure they don't take over.
062 *
063 * @author Arjen Poutsma
064 * @author Rossen Stoyanchev
065 * @author Brian Clozel
066 * @since 3.1
067 */
068public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
069
070        /**
071         * Basic constructor with converters only. Suitable for resolving
072         * {@code HttpEntity}. For handling {@code ResponseEntity} consider also
073         * providing a {@code ContentNegotiationManager}.
074         */
075        public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) {
076                super(converters);
077        }
078
079        /**
080         * Basic constructor with converters and {@code ContentNegotiationManager}.
081         * Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity}
082         * without {@code Request~} or {@code ResponseBodyAdvice}.
083         */
084        public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
085                        ContentNegotiationManager manager) {
086
087                super(converters, manager);
088        }
089
090        /**
091         * Complete constructor for resolving {@code HttpEntity} method arguments.
092         * For handling {@code ResponseEntity} consider also providing a
093         * {@code ContentNegotiationManager}.
094         * @since 4.2
095         */
096        public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
097                        List<Object> requestResponseBodyAdvice) {
098
099                super(converters, null, requestResponseBodyAdvice);
100        }
101
102        /**
103         * Complete constructor for resolving {@code HttpEntity} and handling
104         * {@code ResponseEntity}.
105         */
106        public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
107                        @Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
108
109                super(converters, manager, requestResponseBodyAdvice);
110        }
111
112
113        @Override
114        public boolean supportsParameter(MethodParameter parameter) {
115                return (HttpEntity.class == parameter.getParameterType() ||
116                                RequestEntity.class == parameter.getParameterType());
117        }
118
119        @Override
120        public boolean supportsReturnType(MethodParameter returnType) {
121                return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) &&
122                                !RequestEntity.class.isAssignableFrom(returnType.getParameterType()));
123        }
124
125        @Override
126        @Nullable
127        public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
128                        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
129                        throws IOException, HttpMediaTypeNotSupportedException {
130
131                ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
132                Type paramType = getHttpEntityType(parameter);
133                if (paramType == null) {
134                        throw new IllegalArgumentException("HttpEntity parameter '" + parameter.getParameterName() +
135                                        "' in method " + parameter.getMethod() + " is not parameterized");
136                }
137
138                Object body = readWithMessageConverters(webRequest, parameter, paramType);
139                if (RequestEntity.class == parameter.getParameterType()) {
140                        return new RequestEntity<>(body, inputMessage.getHeaders(),
141                                        inputMessage.getMethod(), inputMessage.getURI());
142                }
143                else {
144                        return new HttpEntity<>(body, inputMessage.getHeaders());
145                }
146        }
147
148        @Nullable
149        private Type getHttpEntityType(MethodParameter parameter) {
150                Assert.isAssignable(HttpEntity.class, parameter.getParameterType());
151                Type parameterType = parameter.getGenericParameterType();
152                if (parameterType instanceof ParameterizedType) {
153                        ParameterizedType type = (ParameterizedType) parameterType;
154                        if (type.getActualTypeArguments().length != 1) {
155                                throw new IllegalArgumentException("Expected single generic parameter on '" +
156                                                parameter.getParameterName() + "' in method " + parameter.getMethod());
157                        }
158                        return type.getActualTypeArguments()[0];
159                }
160                else if (parameterType instanceof Class) {
161                        return Object.class;
162                }
163                else {
164                        return null;
165                }
166        }
167
168        @Override
169        public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
170                        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
171
172                mavContainer.setRequestHandled(true);
173                if (returnValue == null) {
174                        return;
175                }
176
177                ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
178                ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
179
180                Assert.isInstanceOf(HttpEntity.class, returnValue);
181                HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;
182
183                HttpHeaders outputHeaders = outputMessage.getHeaders();
184                HttpHeaders entityHeaders = responseEntity.getHeaders();
185                if (!entityHeaders.isEmpty()) {
186                        entityHeaders.forEach((key, value) -> {
187                                if (HttpHeaders.VARY.equals(key) && outputHeaders.containsKey(HttpHeaders.VARY)) {
188                                        List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders);
189                                        if (!values.isEmpty()) {
190                                                outputHeaders.setVary(values);
191                                        }
192                                }
193                                else {
194                                        outputHeaders.put(key, value);
195                                }
196                        });
197                }
198
199                if (responseEntity instanceof ResponseEntity) {
200                        int returnStatus = ((ResponseEntity<?>) responseEntity).getStatusCodeValue();
201                        outputMessage.getServletResponse().setStatus(returnStatus);
202                        if (returnStatus == 200) {
203                                HttpMethod method = inputMessage.getMethod();
204                                if ((HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method))
205                                                && isResourceNotModified(inputMessage, outputMessage)) {
206                                        outputMessage.flush();
207                                        return;
208                                }
209                        }
210                        else if (returnStatus / 100 == 3) {
211                                String location = outputHeaders.getFirst("location");
212                                if (location != null) {
213                                        saveFlashAttributes(mavContainer, webRequest, location);
214                                }
215                        }
216                }
217
218                // Try even with null body. ResponseBodyAdvice could get involved.
219                writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);
220
221                // Ensure headers are flushed even if no body was written.
222                outputMessage.flush();
223        }
224
225        private List<String> getVaryRequestHeadersToAdd(HttpHeaders responseHeaders, HttpHeaders entityHeaders) {
226                List<String> entityHeadersVary = entityHeaders.getVary();
227                List<String> vary = responseHeaders.get(HttpHeaders.VARY);
228                if (vary != null) {
229                        List<String> result = new ArrayList<>(entityHeadersVary);
230                        for (String header : vary) {
231                                for (String existing : StringUtils.tokenizeToStringArray(header, ",")) {
232                                        if ("*".equals(existing)) {
233                                                return Collections.emptyList();
234                                        }
235                                        for (String value : entityHeadersVary) {
236                                                if (value.equalsIgnoreCase(existing)) {
237                                                        result.remove(value);
238                                                }
239                                        }
240                                }
241                        }
242                        return result;
243                }
244                return entityHeadersVary;
245        }
246
247        private boolean isResourceNotModified(ServletServerHttpRequest request, ServletServerHttpResponse response) {
248                ServletWebRequest servletWebRequest =
249                                new ServletWebRequest(request.getServletRequest(), response.getServletResponse());
250                HttpHeaders responseHeaders = response.getHeaders();
251                String etag = responseHeaders.getETag();
252                long lastModifiedTimestamp = responseHeaders.getLastModified();
253                if (request.getMethod() == HttpMethod.GET || request.getMethod() == HttpMethod.HEAD) {
254                        responseHeaders.remove(HttpHeaders.ETAG);
255                        responseHeaders.remove(HttpHeaders.LAST_MODIFIED);
256                }
257
258                return servletWebRequest.checkNotModified(etag, lastModifiedTimestamp);
259        }
260
261        private void saveFlashAttributes(ModelAndViewContainer mav, NativeWebRequest request, String location) {
262                mav.setRedirectModelScenario(true);
263                ModelMap model = mav.getModel();
264                if (model instanceof RedirectAttributes) {
265                        Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
266                        if (!CollectionUtils.isEmpty(flashAttributes)) {
267                                HttpServletRequest req = request.getNativeRequest(HttpServletRequest.class);
268                                HttpServletResponse res = request.getNativeResponse(HttpServletResponse.class);
269                                if (req != null) {
270                                        RequestContextUtils.getOutputFlashMap(req).putAll(flashAttributes);
271                                        if (res != null) {
272                                                RequestContextUtils.saveOutputFlashMap(location, req, res);
273                                        }
274                                }
275                        }
276                }
277        }
278
279        @Override
280        protected Class<?> getReturnValueType(@Nullable Object returnValue, MethodParameter returnType) {
281                if (returnValue != null) {
282                        return returnValue.getClass();
283                }
284                else {
285                        Type type = getHttpEntityType(returnType);
286                        type = (type != null ? type : Object.class);
287                        return ResolvableType.forMethodParameter(returnType, type).toClass();
288                }
289        }
290
291}