GH-2806: Add generics support to HTTP Inbound

Fixes https://github.com/spring-projects/spring-integration/issues/2806

**Cherry-pick to 5.1.x**
This commit is contained in:
Artem Bilan
2019-03-13 18:54:10 -04:00
committed by Gary Russell
parent 8d9903981e
commit df697c53e2
4 changed files with 137 additions and 73 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2018 the original author or authors.
* Copyright 2017-2019 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.
@@ -66,23 +66,23 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
protected final AtomicInteger activeCount = new AtomicInteger();
private volatile ResolvableType requestPayloadType = null;
private ResolvableType requestPayloadType = null;
private volatile HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private HeaderMapper<HttpHeaders> headerMapper = DefaultHttpHeaderMapper.inboundMapper();
private volatile boolean extractReplyPayload = true;
private boolean extractReplyPayload = true;
private volatile Expression statusCodeExpression;
private Expression statusCodeExpression;
private volatile EvaluationContext evaluationContext;
private EvaluationContext evaluationContext;
private volatile RequestMapping requestMapping = new RequestMapping();
private RequestMapping requestMapping = new RequestMapping();
private volatile Expression payloadExpression;
private Expression payloadExpression;
private volatile Map<String, Expression> headerExpressions;
private Map<String, Expression> headerExpressions;
private volatile CrossOrigin crossOrigin;
private CrossOrigin crossOrigin;
public BaseHttpInboundEndpoint(boolean expectReply) {
super(expectReply);
@@ -279,8 +279,13 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
}
protected HttpStatus evaluateHttpStatus(HttpEntity<?> httpEntity) {
Object value = this.statusCodeExpression.getValue(this.evaluationContext, httpEntity);
return buildHttpStatus(value);
if (this.statusCodeExpression != null) {
Object value = this.statusCodeExpression.getValue(this.evaluationContext, httpEntity);
return buildHttpStatus(value);
}
else {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
protected HttpStatus resolveHttpStatusFromHeaders(MessageHeaders headers) {
@@ -328,8 +333,7 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
* @return true or false if HTTP request can contain the body
*/
protected boolean isReadable(HttpRequest request) {
HttpMethod method = request.getMethod();
return method == null ? false : !(CollectionUtils.containsInstance(nonReadableBodyHttpMethods, method));
return !(CollectionUtils.containsInstance(nonReadableBodyHttpMethods, request.getMethod()));
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.http.inbound;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -39,6 +40,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
@@ -93,7 +95,7 @@ import org.springframework.web.servlet.HandlerMapping;
* In a request-reply scenario, the reply Message's payload will be extracted prior
* to generating a response by default.
* To have the entire serialized Message available for the response, switch the
* {@link #extractReplyPayload} value to {@code false}.
* {@code extractReplyPayload} value to {@code false}.
*
* @author Mark Fisher
* @author Oleg Zhurakousky
@@ -146,27 +148,21 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
stringHttpMessageConverter.setWriteAcceptCharset(false);
this.defaultMessageConverters.add(stringHttpMessageConverter);
this.defaultMessageConverters.add(new ResourceHttpMessageConverter());
SourceHttpMessageConverter<Source> sourceConverter = new SourceHttpMessageConverter<Source>();
SourceHttpMessageConverter<Source> sourceConverter = new SourceHttpMessageConverter<>();
this.defaultMessageConverters.add(sourceConverter);
if (jaxb2Present) {
this.defaultMessageConverters.add(new Jaxb2RootElementHttpMessageConverter());
if (logger.isDebugEnabled()) {
logger.debug("'Jaxb2RootElementHttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
logger.debug("'Jaxb2RootElementHttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
if (JacksonPresent.isJackson2Present()) {
this.defaultMessageConverters.add(new MappingJackson2HttpMessageConverter());
if (logger.isDebugEnabled()) {
logger.debug("'MappingJackson2HttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
logger.debug("'MappingJackson2HttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
if (romeToolsPresent) {
this.defaultMessageConverters.add(new AtomFeedHttpMessageConverter());
this.defaultMessageConverters.add(new RssChannelHttpMessageConverter());
if (logger.isDebugEnabled()) {
logger.debug("'AtomFeedHttpMessageConverter' was added to the 'defaultMessageConverters'.");
logger.debug("'RssChannelHttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
logger.debug("'AtomFeedHttpMessageConverter' was added to the 'defaultMessageConverters'.");
logger.debug("'RssChannelHttpMessageConverter' was added to the 'defaultMessageConverters'.");
}
}
@@ -176,8 +172,9 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
* @param messageConverters The message converters.
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
Assert.notNull(messageConverters, "'messageConverters' must not be null");
Assert.noNullElements(messageConverters.toArray(), "'messageConverters' must not contain null entries");
List<HttpMessageConverter<?>> localConverters = new ArrayList<HttpMessageConverter<?>>(messageConverters);
List<HttpMessageConverter<?>> localConverters = new ArrayList<>(messageConverters);
if (this.mergeWithDefaultConverters) {
localConverters.addAll(this.defaultMessageConverters);
this.convertersMerged = true;
@@ -266,14 +263,13 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
this.activeCount.incrementAndGet();
try {
StandardEvaluationContext evaluationContext = this.createEvaluationContext();
StandardEvaluationContext evaluationContext = createEvaluationContext();
evaluationContext.setRootObject(httpEntity);
evaluationContext.setVariable("requestAttributes", RequestContextHolder.currentRequestAttributes());
MultiValueMap<String, String> requestParams = this.convertParameterMap(servletRequest.getParameterMap());
MultiValueMap<String, String> requestParams = convertParameterMap(servletRequest.getParameterMap());
evaluationContext.setVariable("requestParams", requestParams);
evaluationContext.setVariable("requestHeaders", new ServletServerHttpRequest(servletRequest).getHeaders());
Cookie[] requestCookies = servletRequest.getCookies();
@@ -335,12 +331,16 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
AbstractIntegrationMessageBuilder<?> messageBuilder = null;
if (payload instanceof Message<?>) {
messageBuilder = this.getMessageBuilderFactory().fromMessage((Message<?>) payload)
.copyHeadersIfAbsent(headers);
messageBuilder =
getMessageBuilderFactory()
.fromMessage((Message<?>) payload)
.copyHeadersIfAbsent(headers);
}
else {
Assert.state(payload != null, "payload cannot be null");
messageBuilder = this.getMessageBuilderFactory().withPayload(payload).copyHeaders(headers);
messageBuilder = getMessageBuilderFactory()
.withPayload(payload)
.copyHeaders(headers);
}
HttpMethod method = httpEntity.getMethod();
@@ -359,30 +359,24 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
Message<?> reply = null;
if (this.expectReply) {
try {
reply = this.sendAndReceiveMessage(message);
reply = sendAndReceiveMessage(message);
}
catch (MessageTimeoutException e) {
if (getStatusCodeExpression() != null) {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus(httpEntity))
.build();
}
else {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
HttpStatus.INTERNAL_SERVER_ERROR)
.build();
}
reply =
getMessageBuilderFactory()
.withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus(httpEntity))
.build();
}
}
else {
this.send(message);
send(message);
}
return reply;
}
finally {
this.postProcessRequest(servletRequest);
postProcessRequest(servletRequest);
this.activeCount.decrementAndGet();
}
}
@@ -391,7 +385,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
if (logger.isDebugEnabled()) {
logger.debug("Endpoint is stopped; returning status " + HttpStatus.SERVICE_UNAVAILABLE);
}
return this.getMessageBuilderFactory().withPayload("Endpoint is stopped")
return getMessageBuilderFactory()
.withPayload("Endpoint is stopped")
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE, HttpStatus.SERVICE_UNAVAILABLE)
.build();
}
@@ -401,7 +396,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
* sets up the {@link ServletServerHttpResponse}.
* @param response The ServletServerHttpResponse.
* @param replyMessage The reply message.
* @return The message payload (if {@link #extractReplyPayload}) otherwise the message.
* @return The message payload (if {@code extractReplyPayload}) otherwise the message.
*/
protected final Object setupResponseAndConvertReply(ServletServerHttpResponse response, Message<?> replyMessage) {
getHeaderMapper().fromHeaders(replyMessage.getHeaders(), response.getHeaders());
@@ -461,7 +456,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
* Converts a servlet request's parameterMap to a {@link MultiValueMap}.
*/
private MultiValueMap<String, String> convertParameterMap(Map<String, String[]> parameterMap) {
MultiValueMap<String, String> convertedMap = new LinkedMultiValueMap<String, String>(parameterMap.size());
MultiValueMap<String, String> convertedMap = new LinkedMultiValueMap<>(parameterMap.size());
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String[] values = entry.getValue();
for (String value : values) {
@@ -487,29 +482,37 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
ResolvableType requestPayloadType = getRequestPayloadType();
Class<?> expectedType;
if (requestPayloadType == null) {
expectedType = "text".equals(contentType.getType()) ? String.class : byte[].class;
}
else {
expectedType = requestPayloadType.resolve();
requestPayloadType =
ResolvableType.forClass(
"text".equals(contentType.getType())
? String.class
: byte[].class);
}
/*
* TODO: resolve() can return null, which is not valid for canRead().
* Perhaps we should coerce to String/byte[] instead of attempting
* to convert. However this might be a breaking change - 5.2?
* Hence NOSONAR below.
*/
Type targetType = requestPayloadType.getType();
Class<?> targetClass = requestPayloadType.toClass();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter.canRead(expectedType, contentType)) {
return converter.read((Class) expectedType, request);
GenericHttpMessageConverter<?> genericConverter =
converter instanceof GenericHttpMessageConverter
? (GenericHttpMessageConverter<?>) converter
: null;
if (genericConverter != null
? genericConverter.canRead(targetType, null, contentType) :
(converter.canRead(targetClass, contentType))) {
if (genericConverter != null) {
return genericConverter.read(targetType, null, request);
}
else {
return converter.read((Class) targetClass, request);
}
}
}
throw new MessagingException(// NOSONAR might be null; see comment above.
throw new MessagingException(
"Could not convert request: no suitable HttpMessageConverter found for expected type ["
+ expectedType != null ? expectedType.getName() : "null"
+ "] and content type [" + contentType + "]");
+ requestPayloadType + "] and content type [" + contentType + "]");
}
}

View File

@@ -44,10 +44,11 @@ import org.springframework.web.HttpRequestHandler;
* (e.g. 200 OK).
* <p>
* The default supported request methods are GET and POST, but the list of values can be configured with the
* {@link RequestMapping#methods} property. The payload generated from a GET request (or HEAD or OPTIONS if supported) will
* be a {@link org.springframework.util.MultiValueMap} containing the parameter values. For a request containing a body
* (e.g. a POST), the type
* of the payload is determined by the {@link #setRequestPayloadTypeClass(Class)} request payload type}.
* {@link RequestMapping#getMethods()} property.
* The payload generated from a GET request (or HEAD or OPTIONS if supported) will
* be a {@link org.springframework.util.MultiValueMap} containing the parameter values.
* For a request containing a body (e.g. a POST), the type of the payload is determined
* by the {@link #setRequestPayloadTypeClass(Class)} request payload type}.
* <p>
* If the HTTP request is a multipart and a "multipartResolver" bean has been defined in the context, then it will be
* converted by the

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.http.inbound;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -38,6 +39,8 @@ import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
@@ -64,6 +67,8 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.SerializationUtils;
import org.springframework.web.multipart.MultipartResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Mark Fisher
* @author Gary Russell
@@ -274,9 +279,52 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
assertEquals(TestBean.class, message.getPayload().getClass());
TestBean result = (TestBean) message.getPayload();
assertEquals("T. Bean", result.name);
assertEquals(84, result.age);
assertEquals(42, result.age);
}
@Test
public void testJsonRequestBody() throws Exception {
QueueChannel channel = new QueueChannel();
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(false);
gateway.setBeanFactory(mock(BeanFactory.class));
ParameterizedTypeReference<List<TestBean>> parameterizedTypeReference =
new ParameterizedTypeReference<List<TestBean>>() {
};
gateway.setRequestPayloadType(ResolvableType.forType(parameterizedTypeReference));
gateway.setRequestChannel(channel);
gateway.afterPropertiesSet();
gateway.start();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test");
request.setContentType("application/json");
TestBean testBean = new TestBean();
testBean.setName("T. Bean");
testBean.setAge(42);
request.setContent(new ObjectMapper().writeValueAsBytes(new TestBean[] { testBean }));
MockHttpServletResponse response = new MockHttpServletResponse();
gateway.handleRequest(request, response);
byte[] bytes = response.getContentAsByteArray();
assertThat(bytes).isNotNull();
Message<?> message = channel.receive(0);
assertThat(message).isNotNull()
.extracting(Message::getPayload)
.isInstanceOf(List.class)
.asList()
.hasSize(1)
.element(0)
.isInstanceOf(TestBean.class)
.satisfies((actual) -> {
TestBean bean = (TestBean) actual;
assertThat(bean).extracting(TestBean::getName).isEqualTo("T. Bean");
assertThat(bean).extracting(TestBean::getAge).isEqualTo(42);
});
}
@Test
public void INT2680DuplicateContentTypeHeader() throws Exception {
@@ -496,7 +544,15 @@ public class HttpRequestHandlingMessagingGatewayTests extends AbstractHttpInboun
}
public void setAge(int age) {
this.age = age * 2;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}