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