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}