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}