Skip to content

Commit

Permalink
Support non-Object types for WebFlux requests
Browse files Browse the repository at this point in the history
* Add the support for a `Publisher`, `Resource` and `MultiValueMap`
into the `WebFluxRequestExecutingMessageHandler`
* Along side with the `WebFluxRequestExecutingMessageHandler.setPublisherElementType`
and `WebFluxRequestExecutingMessageHandler.setPublisherElementTypeExpression`,
add XSD support for the `publisher-element-type(-expression)`, which
is used for the element type when request body is a `Publisher`
* Polishing for `AbstractHttpRequestExecutingMessageHandler`

* Fix Sonar smells for affected classes
* Remove used imports

Doc polishing
  • Loading branch information
artembilan authored and garyrussell committed Apr 22, 2019
1 parent 24304ef commit f8f69c9
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 89 deletions.
Expand Up @@ -30,6 +30,9 @@

import javax.xml.transform.Source;

import org.reactivestreams.Publisher;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
Expand All @@ -48,6 +51,7 @@
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.util.Assert;
Expand All @@ -74,7 +78,7 @@
*/
public abstract class AbstractHttpRequestExecutingMessageHandler extends AbstractReplyProducingMessageHandler {

private static final List<HttpMethod> noBodyHttpMethods =
private static final List<HttpMethod> NO_BODY_HTTP_METHODS =
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE);

private final Map<String, Expression> uriVariableExpressions = new HashMap<>();
Expand Down Expand Up @@ -140,7 +144,7 @@ public void setHttpMethodExpression(Expression httpMethodExpression) {
*/
public void setHttpMethod(HttpMethod httpMethod) {
Assert.notNull(httpMethod, "'httpMethod' must not be null");
this.httpMethodExpression = new ValueExpression<HttpMethod>(httpMethod);
this.httpMethodExpression = new ValueExpression<>(httpMethod);
}

/**
Expand Down Expand Up @@ -193,7 +197,7 @@ public void setExpectReply(boolean expectReply) {
*/
public void setExpectedResponseType(Class<?> expectedResponseType) {
Assert.notNull(expectedResponseType, "'expectedResponseType' must not be null");
this.expectedResponseTypeExpression = new ValueExpression<Class<?>>(expectedResponseType);
setExpectedResponseTypeExpression(new ValueExpression<>(expectedResponseType));
}

/**
Expand Down Expand Up @@ -261,20 +265,18 @@ public void setTrustedSpel(boolean trustedSpel) {

@Override
protected void doInit() {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(this.getBeanFactory());
BeanFactory beanFactory = getBeanFactory();
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(beanFactory);
}

@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
HttpMethod httpMethod = determineHttpMethod(requestMessage);

if (!shouldIncludeRequestBody(httpMethod) && this.extractPayloadExplicitlySet) {
if (logger.isWarnEnabled()) {
logger.warn("The 'extractPayload' attribute has no relevance for the current request " +
"since the HTTP Method is '" + httpMethod +
"', and no request body will be sent for that method.");
}
if (this.extractPayloadExplicitlySet && logger.isWarnEnabled() && !shouldIncludeRequestBody(httpMethod)) {
logger.warn("The 'extractPayload' attribute has no relevance for the current request " +
"since the HTTP Method is '" + httpMethod +
"', and no request body will be sent for that method.");
}

Object expectedResponseType = determineExpectedResponseType(requestMessage);
Expand All @@ -293,9 +295,10 @@ private URI generateUri(Message<?> requestMessage) {
"'uriExpression' evaluation must result in a 'String' or 'URI' instance, not: "
+ (uri == null ? "null" : uri.getClass()));
Map<String, ?> uriVariables = determineUriVariables(requestMessage);
UriComponentsBuilder uriComponentsBuilder = uri instanceof String
? UriComponentsBuilder.fromUriString((String) uri)
: UriComponentsBuilder.fromUri((URI) uri);
UriComponentsBuilder uriComponentsBuilder =
uri instanceof String
? UriComponentsBuilder.fromUriString((String) uri)
: UriComponentsBuilder.fromUri((URI) uri);
UriComponents uriComponents = uriComponentsBuilder.buildAndExpand(uriVariables);
try {
return this.encodeUri ? uriComponents.toUri() : new URI(uriComponents.toUriString());
Expand All @@ -310,7 +313,7 @@ protected Object getReply(ResponseEntity<?> httpResponse) {
HttpHeaders httpHeaders = httpResponse.getHeaders();
Map<String, Object> headers = this.headerMapper.toHeaders(httpHeaders);
if (this.transferCookies) {
this.doConvertSetCookie(headers);
doConvertSetCookie(headers);
}

AbstractIntegrationMessageBuilder<?> replyBuilder = null;
Expand Down Expand Up @@ -355,8 +358,9 @@ private void doConvertSetCookie(Map<String, Object> headers) {

private HttpEntity<?> generateHttpRequest(Message<?> message, HttpMethod httpMethod) {
Assert.notNull(message, "message must not be null");
return (this.extractPayload) ? this.createHttpEntityFromPayload(message, httpMethod)
: this.createHttpEntityFromMessage(message, httpMethod);
return this.extractPayload
? createHttpEntityFromPayload(message, httpMethod)
: createHttpEntityFromMessage(message, httpMethod);
}

private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod httpMethod) {
Expand All @@ -371,16 +375,20 @@ private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod
}
// otherwise, we are creating a request with a body and need to deal with the content-type header as well
if (httpHeaders.getContentType() == null) {
MediaType contentType = (payload instanceof String)
? resolveContentType((String) payload, this.charset)
: resolveContentType(payload);
MediaType contentType =
payload instanceof String
? new MediaType("text", "plain", this.charset)
: resolveContentType(payload);
httpHeaders.setContentType(contentType);
}
if (MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType())) {
if (!(payload instanceof MultiValueMap)) {
payload = this.convertToMultiValueMap((Map<?, ?>) payload);
}
if ((MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType()))
&& !(payload instanceof MultiValueMap)) {

Assert.isInstanceOf(Map.class, payload,
() -> "For " + MediaType.APPLICATION_FORM_URLENCODED + " and " +
MediaType.MULTIPART_FORM_DATA + " media types the payload must be an instance of a Map.");
payload = convertToMultiValueMap((Map<?, ?>) payload);
}
return new HttpEntity<>(payload, httpHeaders);
}
Expand Down Expand Up @@ -409,42 +417,36 @@ private MediaType resolveContentType(Object content) {
else if (content instanceof Source) {
contentType = MediaType.TEXT_XML;
}
else if (content instanceof Map) {
else if (content instanceof Map && isFormData((Map<Object, ?>) content)) {
// We need to check separately for MULTIPART as well as URLENCODED simply because
// MultiValueMap<Object, Object> is actually valid content for serialization
if (this.isFormData((Map<Object, ?>) content)) {
if (this.isMultipart((Map<String, ?>) content)) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
else {
contentType = MediaType.APPLICATION_FORM_URLENCODED;
}
if (isMultipart((Map<String, ?>) content)) {
contentType = MediaType.MULTIPART_FORM_DATA;
}
else {
contentType = MediaType.APPLICATION_FORM_URLENCODED;
}
}
if (contentType == null) {
if (contentType == null && !(content instanceof Publisher<?>)) {
contentType = new MediaType("application", "x-java-serialized-object");
}
return contentType;
}

private boolean shouldIncludeRequestBody(HttpMethod httpMethod) {
return !(CollectionUtils.containsInstance(noBodyHttpMethods, httpMethod));
}

private MediaType resolveContentType(String content, Charset charset) {
return new MediaType("text", "plain", charset);
return !(CollectionUtils.containsInstance(NO_BODY_HTTP_METHODS, httpMethod));
}

private MultiValueMap<Object, Object> convertToMultiValueMap(Map<?, ?> simpleMap) {
LinkedMultiValueMap<Object, Object> multipartValueMap = new LinkedMultiValueMap<Object, Object>();
LinkedMultiValueMap<Object, Object> multipartValueMap = new LinkedMultiValueMap<>();
for (Entry<?, ?> entry : simpleMap.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Object[]) {
value = Arrays.asList((Object[]) value);
}
if (value instanceof Collection) {
multipartValueMap.put(key, new ArrayList<Object>((Collection<?>) value));
multipartValueMap.put(key, new ArrayList<>((Collection<?>) value));
}
else {
multipartValueMap.add(key, value);
Expand All @@ -453,6 +455,18 @@ private MultiValueMap<Object, Object> convertToMultiValueMap(Map<?, ?> simpleMap
return multipartValueMap;
}

/**
* If all keys and values are Strings, we'll consider the Map to be form data.
*/
private boolean isFormData(Map<Object, ?> map) {
for (Object key : map.keySet()) {
if (!(key instanceof String)) {
return false;
}
}
return true;
}

/**
* If all keys are Strings, and some values are not Strings we'll consider
* the Map to be multipart/form-data
Expand All @@ -479,21 +493,9 @@ else if (!(value instanceof String)) {
return false;
}

/**
* If all keys and values are Strings, we'll consider the Map to be form data.
*/
private boolean isFormData(Map<Object, ?> map) {
for (Object key : map.keySet()) {
if (!(key instanceof String)) {
return false;
}
}
return true;
}

private HttpMethod determineHttpMethod(Message<?> requestMessage) {
Object httpMethod = this.httpMethodExpression.getValue(this.evaluationContext, requestMessage);
Assert.state(httpMethod != null && (httpMethod instanceof String || httpMethod instanceof HttpMethod),
Assert.state((httpMethod instanceof String || httpMethod instanceof HttpMethod), () ->
"'httpMethodExpression' evaluation must result in an 'HttpMethod' enum or its String representation, " +
"not: " + (httpMethod == null ? "null" : httpMethod.getClass()));
if (httpMethod instanceof HttpMethod) {
Expand All @@ -503,36 +505,43 @@ private HttpMethod determineHttpMethod(Message<?> requestMessage) {
try {
return HttpMethod.valueOf((String) httpMethod);
}
catch (Exception e) {
catch (Exception ex) {
throw new IllegalStateException("The 'httpMethodExpression' returned an invalid HTTP Method value: "
+ httpMethod);
+ httpMethod, ex);
}
}
}

private Object determineExpectedResponseType(Message<?> requestMessage) {
Object expectedResponseType = null;
if (this.expectedResponseTypeExpression != null) {
expectedResponseType = this.expectedResponseTypeExpression.getValue(this.evaluationContext, requestMessage);
return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType");
}

@Nullable
protected Object evaluateTypeFromExpression(Message<?> requestMessage, @Nullable Expression expression,
String property) {

Object type = null;
if (expression != null) {
type = expression.getValue(this.evaluationContext, requestMessage);
}
if (expectedResponseType != null) {
Assert.state(expectedResponseType instanceof Class<?>
|| expectedResponseType instanceof String
|| expectedResponseType instanceof ParameterizedTypeReference,
"'expectedResponseType' can be an instance of 'Class<?>', 'String' " +
if (type != null) {
Class<?> typeClass = type.getClass();
Assert.state(type instanceof Class<?>
|| type instanceof String
|| type instanceof ParameterizedTypeReference,
() -> "The '" + property + "' can be an instance of 'Class<?>', 'String' " +
"or 'ParameterizedTypeReference<?>'; " +
"evaluation resulted in a" + expectedResponseType.getClass() + ".");
if (expectedResponseType instanceof String && StringUtils.hasText((String) expectedResponseType)) {
"evaluation resulted in a " + typeClass + ".");
if (type instanceof String && StringUtils.hasText((String) type)) {
try {
expectedResponseType = ClassUtils.forName((String) expectedResponseType,
getApplicationContext().getClassLoader());
type = ClassUtils.forName((String) type, getApplicationContext().getClassLoader());
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Cannot load class for name: " + expectedResponseType, e);
throw new IllegalStateException("Cannot load class for name: " + type, e);
}
}
}
return expectedResponseType;
return type;
}

@SuppressWarnings("unchecked")
Expand Down
Expand Up @@ -21,6 +21,7 @@
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.http.config.HttpOutboundChannelAdapterParser;
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;
Expand All @@ -36,6 +37,12 @@ public class WebFluxOutboundChannelAdapterParser extends HttpOutboundChannelAdap

@Override
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
return buildWebFluxRequestExecutingMessageHandler(element, parserContext);
}

static BeanDefinitionBuilder buildWebFluxRequestExecutingMessageHandler(Element element,
ParserContext parserContext) {

BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);

Expand All @@ -46,6 +53,28 @@ protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parser
.addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef));
}

String type = element.getAttribute("publisher-element-type");
String typeExpression = element.getAttribute("publisher-element-type-expression");

boolean hasType = StringUtils.hasText(type);
boolean hasTypeExpression = StringUtils.hasText(typeExpression);

if (hasType && hasTypeExpression) {
parserContext.getReaderContext()
.error("The 'publisher-element-type' and 'publisher-element-type-expression' " +
"are mutually exclusive. You can only have one or the other", element);
}

if (hasType) {
builder.addPropertyValue("publisherElementType", type);
}
else if (hasTypeExpression) {
builder.addPropertyValue("publisherElementTypeExpression",
BeanDefinitionBuilder.rootBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(typeExpression)
.getBeanDefinition());
}

return builder;
}

Expand Down
Expand Up @@ -18,13 +18,10 @@

import org.w3c.dom.Element;

import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.config.HttpOutboundGatewayParser;
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
import org.springframework.util.StringUtils;

/**
* Parser for the 'outbound-gateway' element of the webflux namespace.
Expand All @@ -38,15 +35,7 @@ public class WebFluxOutboundGatewayParser extends HttpOutboundGatewayParser {
@Override
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);

String webClientRef = element.getAttribute("web-client");
if (StringUtils.hasText(webClientRef)) {
builder.getBeanDefinition()
.getConstructorArgumentValues()
.addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef));
}

WebFluxOutboundChannelAdapterParser.buildWebFluxRequestExecutingMessageHandler(element, parserContext);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-payload-to-flux");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "body-extractor");
return builder;
Expand Down

0 comments on commit f8f69c9

Please sign in to comment.