/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.support.RequestContextUtils;
/**
* Resolves {@link HttpEntity} and {@link RequestEntity} method argument values
* and also handles {@link HttpEntity} and {@link ResponseEntity} return values.
*
* <p>An {@link HttpEntity} return type has a specific purpose. Therefore this
* handler should be configured ahead of handlers that support any return
* value type annotated with {@code @ModelAttribute} or {@code @ResponseBody}
* to ensure they don't take over.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 3.1
*/
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
/**
* Basic constructor with converters only. Suitable for resolving
* {@code HttpEntity}. For handling {@code ResponseEntity} consider also
* providing a {@code ContentNegotiationManager}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
}
/**
* Basic constructor with converters and {@code ContentNegotiationManager}.
* Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity}
* without {@code Request~} or {@code ResponseBodyAdvice}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager) {
super(converters, manager);
}
/**
* Complete constructor for resolving {@code HttpEntity} method arguments.
* For handling {@code ResponseEntity} consider also providing a
* {@code ContentNegotiationManager}.
* @since 4.2
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
List<Object> requestResponseBodyAdvice) {
super(converters, null, requestResponseBodyAdvice);
}
/**
* Complete constructor for resolving {@code HttpEntity} and handling
* {@code ResponseEntity}.
*/
public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
super(converters, manager, requestResponseBodyAdvice);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (HttpEntity.class == parameter.getParameterType() ||
RequestEntity.class == parameter.getParameterType());
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) &&
!RequestEntity.class.isAssignableFrom(returnType.getParameterType()));
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
throws IOException, HttpMediaTypeNotSupportedException {
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
Type paramType = getHttpEntityType(parameter);
if (paramType == null) {
throw new IllegalArgumentException("HttpEntity parameter '" + parameter.getParameterName() +
"' in method " + parameter.getMethod() + " is not parameterized");
}
Object body = readWithMessageConverters(webRequest, parameter, paramType);
if (RequestEntity.class == parameter.getParameterType()) {
return new RequestEntity<>(body, inputMessage.getHeaders(),
inputMessage.getMethod(), inputMessage.getURI());
}
else {
return new HttpEntity<>(body, inputMessage.getHeaders());
}
}
private Type getHttpEntityType(MethodParameter parameter) {
Assert.isAssignable(HttpEntity.class, parameter.getParameterType());
Type parameterType = parameter.getGenericParameterType();
if (parameterType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) parameterType;
if (type.getActualTypeArguments().length != 1) {
throw new IllegalArgumentException("Expected single generic parameter on '" +
parameter.getParameterName() + "' in method " + parameter.getMethod());
}
return type.getActualTypeArguments()[0];
}
else if (parameterType instanceof Class) {
return Object.class;
}
else {
return null;
}
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true);
if (returnValue == null) {
return;
}
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
Assert.isInstanceOf(HttpEntity.class, returnValue);
HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;
HttpHeaders outputHeaders = outputMessage.getHeaders();
HttpHeaders entityHeaders = responseEntity.getHeaders();
if (outputHeaders.containsKey(HttpHeaders.VARY) && entityHeaders.containsKey(HttpHeaders.VARY)) {
List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders);
if (!values.isEmpty()) {
outputHeaders.setVary(values);
}
}
if (!entityHeaders.isEmpty()) {
for (Map.Entry<String, List<String>> entry : entityHeaders.entrySet()) {
if (!outputHeaders.containsKey(entry.getKey())) {
outputHeaders.put(entry.getKey(), entry.getValue());
}
}
}
if (responseEntity instanceof ResponseEntity) {
int returnStatus = ((ResponseEntity<?>) responseEntity).getStatusCodeValue();
outputMessage.getServletResponse().setStatus(returnStatus);
if (returnStatus == 200) {
if (isResourceNotModified(inputMessage, outputMessage)) {
// Ensure headers are flushed, no body should be written.
outputMessage.flush();
// Skip call to converters, as they may update the body.
return;
}
}
else if (returnStatus / 100 == 3) {
String location = outputHeaders.getFirst("location");
if (location != null) {
saveFlashAttributes(mavContainer, webRequest, location);
}
}
}
// Try even with null body. ResponseBodyAdvice could get involved.
writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);
// Ensure headers are flushed even if no body was written.
outputMessage.flush();
}
private List<String> getVaryRequestHeadersToAdd(HttpHeaders responseHeaders, HttpHeaders entityHeaders) {
if (!responseHeaders.containsKey(HttpHeaders.VARY)) {
return entityHeaders.getVary();
}
List<String> entityHeadersVary = entityHeaders.getVary();
List<String> result = new ArrayList<>(entityHeadersVary);
for (String header : responseHeaders.get(HttpHeaders.VARY)) {
for (String existing : StringUtils.tokenizeToStringArray(header, ",")) {
if ("*".equals(existing)) {
return Collections.emptyList();
}
for (String value : entityHeadersVary) {
if (value.equalsIgnoreCase(existing)) {
result.remove(value);
}
}
}
}
return result;
}
private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
ServletWebRequest servletWebRequest =
new ServletWebRequest(inputMessage.getServletRequest(), outputMessage.getServletResponse());
HttpHeaders responseHeaders = outputMessage.getHeaders();
String etag = responseHeaders.getETag();
long lastModifiedTimestamp = responseHeaders.getLastModified();
if (inputMessage.getMethod() == HttpMethod.GET || inputMessage.getMethod() == HttpMethod.HEAD) {
responseHeaders.remove(HttpHeaders.ETAG);
responseHeaders.remove(HttpHeaders.LAST_MODIFIED);
}
return servletWebRequest.checkNotModified(etag, lastModifiedTimestamp);
}
private void saveFlashAttributes(ModelAndViewContainer mav, NativeWebRequest request, String location) {
mav.setRedirectModelScenario(true);
ModelMap model = mav.getModel();
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
if (!CollectionUtils.isEmpty(flashAttributes)) {
HttpServletRequest req = request.getNativeRequest(HttpServletRequest.class);
HttpServletResponse res = request.getNativeRequest(HttpServletResponse.class);
RequestContextUtils.getOutputFlashMap(req).putAll(flashAttributes);
RequestContextUtils.saveOutputFlashMap(location, req, res);
}
}
}
@Override
protected Class<?> getReturnValueType(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
return returnValue.getClass();
}
else {
Type type = getHttpEntityType(returnType);
type = (type != null ? type : Object.class);
return ResolvableType.forMethodParameter(returnType, type).resolve(Object.class);
}
}
}