All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2022 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
 *
 *      https://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.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.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.ProblemDetail;
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.lang.Nullable;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.ErrorResponse;
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,
 * as well as return values of type {@link HttpEntity}, {@link ResponseEntity},
 * {@link ErrorResponse} and {@link ProblemDetail}.
 *
 * 

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 * @author Sam Brannen * @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> 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> 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> converters, List requestResponseBodyAdvice) { super(converters, null, requestResponseBodyAdvice); } /** * Complete constructor for resolving {@code HttpEntity} and handling * {@code ResponseEntity}. */ public HttpEntityMethodProcessor(List> converters, @Nullable ContentNegotiationManager manager, List 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) { Class type = returnType.getParameterType(); return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) || ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type)); } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable 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()); } } @Nullable private Type getHttpEntityType(MethodParameter parameter) { Assert.isAssignable(HttpEntity.class, parameter.getParameterType()); Type parameterType = parameter.getGenericParameterType(); if (parameterType instanceof ParameterizedType type) { 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(@Nullable 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); HttpEntity httpEntity; if (returnValue instanceof ErrorResponse response) { httpEntity = new ResponseEntity<>(response.getBody(), response.getHeaders(), response.getStatusCode()); } else if (returnValue instanceof ProblemDetail detail) { httpEntity = ResponseEntity.of(detail).build(); } else { Assert.isInstanceOf(HttpEntity.class, returnValue); httpEntity = (HttpEntity) returnValue; } if (httpEntity.getBody() instanceof ProblemDetail detail) { if (detail.getInstance() == null) { URI path = URI.create(inputMessage.getServletRequest().getRequestURI()); detail.setInstance(path); } if (logger.isWarnEnabled() && httpEntity instanceof ResponseEntity responseEntity) { if (responseEntity.getStatusCode().value() != detail.getStatus()) { logger.warn(returnType.getExecutable().toGenericString() + " returned ResponseEntity: " + responseEntity + ", but its status" + " doesn't match the ProblemDetail status: " + detail.getStatus()); } } } HttpHeaders outputHeaders = outputMessage.getHeaders(); HttpHeaders entityHeaders = httpEntity.getHeaders(); if (!entityHeaders.isEmpty()) { entityHeaders.forEach((key, value) -> { if (HttpHeaders.VARY.equals(key) && outputHeaders.containsKey(HttpHeaders.VARY)) { List values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders); if (!values.isEmpty()) { outputHeaders.setVary(values); } } else { outputHeaders.put(key, value); } }); } if (httpEntity instanceof ResponseEntity responseEntity) { int returnStatus = responseEntity.getStatusCode().value(); outputMessage.getServletResponse().setStatus(returnStatus); if (returnStatus == 200) { HttpMethod method = inputMessage.getMethod(); if ((HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method)) && isResourceNotModified(inputMessage, outputMessage)) { outputMessage.flush(); 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(httpEntity.getBody(), returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. outputMessage.flush(); } private List getVaryRequestHeadersToAdd(HttpHeaders responseHeaders, HttpHeaders entityHeaders) { List entityHeadersVary = entityHeaders.getVary(); List vary = responseHeaders.get(HttpHeaders.VARY); if (vary != null) { List result = new ArrayList<>(entityHeadersVary); for (String header : 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; } return entityHeadersVary; } private boolean isResourceNotModified(ServletServerHttpRequest request, ServletServerHttpResponse response) { ServletWebRequest servletWebRequest = new ServletWebRequest(request.getServletRequest(), response.getServletResponse()); HttpHeaders responseHeaders = response.getHeaders(); String etag = responseHeaders.getETag(); long lastModifiedTimestamp = responseHeaders.getLastModified(); if (request.getMethod() == HttpMethod.GET || request.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 redirectAttributes) { Map flashAttributes = redirectAttributes.getFlashAttributes(); if (!CollectionUtils.isEmpty(flashAttributes)) { HttpServletRequest req = request.getNativeRequest(HttpServletRequest.class); HttpServletResponse res = request.getNativeResponse(HttpServletResponse.class); if (req != null) { RequestContextUtils.getOutputFlashMap(req).putAll(flashAttributes); if (res != null) { RequestContextUtils.saveOutputFlashMap(location, req, res); } } } } } @Override protected Class getReturnValueType(@Nullable 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).toClass(); } } }