Skip to content

Commit f8f69c9

Browse files
artembilangaryrussell
authored andcommitted
Support non-Object types for WebFlux requests
* 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
1 parent 24304ef commit f8f69c9

File tree

12 files changed

+279
-89
lines changed

12 files changed

+279
-89
lines changed

spring-integration-http/src/main/java/org/springframework/integration/http/outbound/AbstractHttpRequestExecutingMessageHandler.java

+80-71
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030

3131
import javax.xml.transform.Source;
3232

33+
import org.reactivestreams.Publisher;
34+
35+
import org.springframework.beans.factory.BeanFactory;
3336
import org.springframework.core.ParameterizedTypeReference;
3437
import org.springframework.expression.EvaluationContext;
3538
import org.springframework.expression.Expression;
@@ -48,6 +51,7 @@
4851
import org.springframework.integration.mapping.HeaderMapper;
4952
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
5053
import org.springframework.integration.support.MessageBuilderFactory;
54+
import org.springframework.lang.Nullable;
5155
import org.springframework.messaging.Message;
5256
import org.springframework.messaging.MessageHandlingException;
5357
import org.springframework.util.Assert;
@@ -74,7 +78,7 @@
7478
*/
7579
public abstract class AbstractHttpRequestExecutingMessageHandler extends AbstractReplyProducingMessageHandler {
7680

77-
private static final List<HttpMethod> noBodyHttpMethods =
81+
private static final List<HttpMethod> NO_BODY_HTTP_METHODS =
7882
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE);
7983

8084
private final Map<String, Expression> uriVariableExpressions = new HashMap<>();
@@ -140,7 +144,7 @@ public void setHttpMethodExpression(Expression httpMethodExpression) {
140144
*/
141145
public void setHttpMethod(HttpMethod httpMethod) {
142146
Assert.notNull(httpMethod, "'httpMethod' must not be null");
143-
this.httpMethodExpression = new ValueExpression<HttpMethod>(httpMethod);
147+
this.httpMethodExpression = new ValueExpression<>(httpMethod);
144148
}
145149

146150
/**
@@ -193,7 +197,7 @@ public void setExpectReply(boolean expectReply) {
193197
*/
194198
public void setExpectedResponseType(Class<?> expectedResponseType) {
195199
Assert.notNull(expectedResponseType, "'expectedResponseType' must not be null");
196-
this.expectedResponseTypeExpression = new ValueExpression<Class<?>>(expectedResponseType);
200+
setExpectedResponseTypeExpression(new ValueExpression<>(expectedResponseType));
197201
}
198202

199203
/**
@@ -261,20 +265,18 @@ public void setTrustedSpel(boolean trustedSpel) {
261265

262266
@Override
263267
protected void doInit() {
264-
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
265-
this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(this.getBeanFactory());
268+
BeanFactory beanFactory = getBeanFactory();
269+
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
270+
this.simpleEvaluationContext = ExpressionUtils.createSimpleEvaluationContext(beanFactory);
266271
}
267272

268273
@Override
269274
protected Object handleRequestMessage(Message<?> requestMessage) {
270275
HttpMethod httpMethod = determineHttpMethod(requestMessage);
271-
272-
if (!shouldIncludeRequestBody(httpMethod) && this.extractPayloadExplicitlySet) {
273-
if (logger.isWarnEnabled()) {
274-
logger.warn("The 'extractPayload' attribute has no relevance for the current request " +
275-
"since the HTTP Method is '" + httpMethod +
276-
"', and no request body will be sent for that method.");
277-
}
276+
if (this.extractPayloadExplicitlySet && logger.isWarnEnabled() && !shouldIncludeRequestBody(httpMethod)) {
277+
logger.warn("The 'extractPayload' attribute has no relevance for the current request " +
278+
"since the HTTP Method is '" + httpMethod +
279+
"', and no request body will be sent for that method.");
278280
}
279281

280282
Object expectedResponseType = determineExpectedResponseType(requestMessage);
@@ -293,9 +295,10 @@ private URI generateUri(Message<?> requestMessage) {
293295
"'uriExpression' evaluation must result in a 'String' or 'URI' instance, not: "
294296
+ (uri == null ? "null" : uri.getClass()));
295297
Map<String, ?> uriVariables = determineUriVariables(requestMessage);
296-
UriComponentsBuilder uriComponentsBuilder = uri instanceof String
297-
? UriComponentsBuilder.fromUriString((String) uri)
298-
: UriComponentsBuilder.fromUri((URI) uri);
298+
UriComponentsBuilder uriComponentsBuilder =
299+
uri instanceof String
300+
? UriComponentsBuilder.fromUriString((String) uri)
301+
: UriComponentsBuilder.fromUri((URI) uri);
299302
UriComponents uriComponents = uriComponentsBuilder.buildAndExpand(uriVariables);
300303
try {
301304
return this.encodeUri ? uriComponents.toUri() : new URI(uriComponents.toUriString());
@@ -310,7 +313,7 @@ protected Object getReply(ResponseEntity<?> httpResponse) {
310313
HttpHeaders httpHeaders = httpResponse.getHeaders();
311314
Map<String, Object> headers = this.headerMapper.toHeaders(httpHeaders);
312315
if (this.transferCookies) {
313-
this.doConvertSetCookie(headers);
316+
doConvertSetCookie(headers);
314317
}
315318

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

356359
private HttpEntity<?> generateHttpRequest(Message<?> message, HttpMethod httpMethod) {
357360
Assert.notNull(message, "message must not be null");
358-
return (this.extractPayload) ? this.createHttpEntityFromPayload(message, httpMethod)
359-
: this.createHttpEntityFromMessage(message, httpMethod);
361+
return this.extractPayload
362+
? createHttpEntityFromPayload(message, httpMethod)
363+
: createHttpEntityFromMessage(message, httpMethod);
360364
}
361365

362366
private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod httpMethod) {
@@ -371,16 +375,20 @@ private HttpEntity<?> createHttpEntityFromPayload(Message<?> message, HttpMethod
371375
}
372376
// otherwise, we are creating a request with a body and need to deal with the content-type header as well
373377
if (httpHeaders.getContentType() == null) {
374-
MediaType contentType = (payload instanceof String)
375-
? resolveContentType((String) payload, this.charset)
376-
: resolveContentType(payload);
378+
MediaType contentType =
379+
payload instanceof String
380+
? new MediaType("text", "plain", this.charset)
381+
: resolveContentType(payload);
377382
httpHeaders.setContentType(contentType);
378383
}
379-
if (MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
380-
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType())) {
381-
if (!(payload instanceof MultiValueMap)) {
382-
payload = this.convertToMultiValueMap((Map<?, ?>) payload);
383-
}
384+
if ((MediaType.APPLICATION_FORM_URLENCODED.equals(httpHeaders.getContentType()) ||
385+
MediaType.MULTIPART_FORM_DATA.equals(httpHeaders.getContentType()))
386+
&& !(payload instanceof MultiValueMap)) {
387+
388+
Assert.isInstanceOf(Map.class, payload,
389+
() -> "For " + MediaType.APPLICATION_FORM_URLENCODED + " and " +
390+
MediaType.MULTIPART_FORM_DATA + " media types the payload must be an instance of a Map.");
391+
payload = convertToMultiValueMap((Map<?, ?>) payload);
384392
}
385393
return new HttpEntity<>(payload, httpHeaders);
386394
}
@@ -409,42 +417,36 @@ private MediaType resolveContentType(Object content) {
409417
else if (content instanceof Source) {
410418
contentType = MediaType.TEXT_XML;
411419
}
412-
else if (content instanceof Map) {
420+
else if (content instanceof Map && isFormData((Map<Object, ?>) content)) {
413421
// We need to check separately for MULTIPART as well as URLENCODED simply because
414422
// MultiValueMap<Object, Object> is actually valid content for serialization
415-
if (this.isFormData((Map<Object, ?>) content)) {
416-
if (this.isMultipart((Map<String, ?>) content)) {
417-
contentType = MediaType.MULTIPART_FORM_DATA;
418-
}
419-
else {
420-
contentType = MediaType.APPLICATION_FORM_URLENCODED;
421-
}
423+
if (isMultipart((Map<String, ?>) content)) {
424+
contentType = MediaType.MULTIPART_FORM_DATA;
425+
}
426+
else {
427+
contentType = MediaType.APPLICATION_FORM_URLENCODED;
422428
}
423429
}
424-
if (contentType == null) {
430+
if (contentType == null && !(content instanceof Publisher<?>)) {
425431
contentType = new MediaType("application", "x-java-serialized-object");
426432
}
427433
return contentType;
428434
}
429435

430436
private boolean shouldIncludeRequestBody(HttpMethod httpMethod) {
431-
return !(CollectionUtils.containsInstance(noBodyHttpMethods, httpMethod));
432-
}
433-
434-
private MediaType resolveContentType(String content, Charset charset) {
435-
return new MediaType("text", "plain", charset);
437+
return !(CollectionUtils.containsInstance(NO_BODY_HTTP_METHODS, httpMethod));
436438
}
437439

438440
private MultiValueMap<Object, Object> convertToMultiValueMap(Map<?, ?> simpleMap) {
439-
LinkedMultiValueMap<Object, Object> multipartValueMap = new LinkedMultiValueMap<Object, Object>();
441+
LinkedMultiValueMap<Object, Object> multipartValueMap = new LinkedMultiValueMap<>();
440442
for (Entry<?, ?> entry : simpleMap.entrySet()) {
441443
Object key = entry.getKey();
442444
Object value = entry.getValue();
443445
if (value instanceof Object[]) {
444446
value = Arrays.asList((Object[]) value);
445447
}
446448
if (value instanceof Collection) {
447-
multipartValueMap.put(key, new ArrayList<Object>((Collection<?>) value));
449+
multipartValueMap.put(key, new ArrayList<>((Collection<?>) value));
448450
}
449451
else {
450452
multipartValueMap.add(key, value);
@@ -453,6 +455,18 @@ private MultiValueMap<Object, Object> convertToMultiValueMap(Map<?, ?> simpleMap
453455
return multipartValueMap;
454456
}
455457

458+
/**
459+
* If all keys and values are Strings, we'll consider the Map to be form data.
460+
*/
461+
private boolean isFormData(Map<Object, ?> map) {
462+
for (Object key : map.keySet()) {
463+
if (!(key instanceof String)) {
464+
return false;
465+
}
466+
}
467+
return true;
468+
}
469+
456470
/**
457471
* If all keys are Strings, and some values are not Strings we'll consider
458472
* the Map to be multipart/form-data
@@ -479,21 +493,9 @@ else if (!(value instanceof String)) {
479493
return false;
480494
}
481495

482-
/**
483-
* If all keys and values are Strings, we'll consider the Map to be form data.
484-
*/
485-
private boolean isFormData(Map<Object, ?> map) {
486-
for (Object key : map.keySet()) {
487-
if (!(key instanceof String)) {
488-
return false;
489-
}
490-
}
491-
return true;
492-
}
493-
494496
private HttpMethod determineHttpMethod(Message<?> requestMessage) {
495497
Object httpMethod = this.httpMethodExpression.getValue(this.evaluationContext, requestMessage);
496-
Assert.state(httpMethod != null && (httpMethod instanceof String || httpMethod instanceof HttpMethod),
498+
Assert.state((httpMethod instanceof String || httpMethod instanceof HttpMethod), () ->
497499
"'httpMethodExpression' evaluation must result in an 'HttpMethod' enum or its String representation, " +
498500
"not: " + (httpMethod == null ? "null" : httpMethod.getClass()));
499501
if (httpMethod instanceof HttpMethod) {
@@ -503,36 +505,43 @@ private HttpMethod determineHttpMethod(Message<?> requestMessage) {
503505
try {
504506
return HttpMethod.valueOf((String) httpMethod);
505507
}
506-
catch (Exception e) {
508+
catch (Exception ex) {
507509
throw new IllegalStateException("The 'httpMethodExpression' returned an invalid HTTP Method value: "
508-
+ httpMethod);
510+
+ httpMethod, ex);
509511
}
510512
}
511513
}
512514

513515
private Object determineExpectedResponseType(Message<?> requestMessage) {
514-
Object expectedResponseType = null;
515-
if (this.expectedResponseTypeExpression != null) {
516-
expectedResponseType = this.expectedResponseTypeExpression.getValue(this.evaluationContext, requestMessage);
516+
return evaluateTypeFromExpression(requestMessage, this.expectedResponseTypeExpression, "expectedResponseType");
517+
}
518+
519+
@Nullable
520+
protected Object evaluateTypeFromExpression(Message<?> requestMessage, @Nullable Expression expression,
521+
String property) {
522+
523+
Object type = null;
524+
if (expression != null) {
525+
type = expression.getValue(this.evaluationContext, requestMessage);
517526
}
518-
if (expectedResponseType != null) {
519-
Assert.state(expectedResponseType instanceof Class<?>
520-
|| expectedResponseType instanceof String
521-
|| expectedResponseType instanceof ParameterizedTypeReference,
522-
"'expectedResponseType' can be an instance of 'Class<?>', 'String' " +
527+
if (type != null) {
528+
Class<?> typeClass = type.getClass();
529+
Assert.state(type instanceof Class<?>
530+
|| type instanceof String
531+
|| type instanceof ParameterizedTypeReference,
532+
() -> "The '" + property + "' can be an instance of 'Class<?>', 'String' " +
523533
"or 'ParameterizedTypeReference<?>'; " +
524-
"evaluation resulted in a" + expectedResponseType.getClass() + ".");
525-
if (expectedResponseType instanceof String && StringUtils.hasText((String) expectedResponseType)) {
534+
"evaluation resulted in a " + typeClass + ".");
535+
if (type instanceof String && StringUtils.hasText((String) type)) {
526536
try {
527-
expectedResponseType = ClassUtils.forName((String) expectedResponseType,
528-
getApplicationContext().getClassLoader());
537+
type = ClassUtils.forName((String) type, getApplicationContext().getClassLoader());
529538
}
530539
catch (ClassNotFoundException e) {
531-
throw new IllegalStateException("Cannot load class for name: " + expectedResponseType, e);
540+
throw new IllegalStateException("Cannot load class for name: " + type, e);
532541
}
533542
}
534543
}
535-
return expectedResponseType;
544+
return type;
536545
}
537546

538547
@SuppressWarnings("unchecked")

spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundChannelAdapterParser.java

+29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.beans.factory.config.RuntimeBeanReference;
2222
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2323
import org.springframework.beans.factory.xml.ParserContext;
24+
import org.springframework.integration.config.ExpressionFactoryBean;
2425
import org.springframework.integration.http.config.HttpOutboundChannelAdapterParser;
2526
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
2627
import org.springframework.util.StringUtils;
@@ -36,6 +37,12 @@ public class WebFluxOutboundChannelAdapterParser extends HttpOutboundChannelAdap
3637

3738
@Override
3839
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
40+
return buildWebFluxRequestExecutingMessageHandler(element, parserContext);
41+
}
42+
43+
static BeanDefinitionBuilder buildWebFluxRequestExecutingMessageHandler(Element element,
44+
ParserContext parserContext) {
45+
3946
BeanDefinitionBuilder builder =
4047
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);
4148

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

56+
String type = element.getAttribute("publisher-element-type");
57+
String typeExpression = element.getAttribute("publisher-element-type-expression");
58+
59+
boolean hasType = StringUtils.hasText(type);
60+
boolean hasTypeExpression = StringUtils.hasText(typeExpression);
61+
62+
if (hasType && hasTypeExpression) {
63+
parserContext.getReaderContext()
64+
.error("The 'publisher-element-type' and 'publisher-element-type-expression' " +
65+
"are mutually exclusive. You can only have one or the other", element);
66+
}
67+
68+
if (hasType) {
69+
builder.addPropertyValue("publisherElementType", type);
70+
}
71+
else if (hasTypeExpression) {
72+
builder.addPropertyValue("publisherElementTypeExpression",
73+
BeanDefinitionBuilder.rootBeanDefinition(ExpressionFactoryBean.class)
74+
.addConstructorArgValue(typeExpression)
75+
.getBeanDefinition());
76+
}
77+
4978
return builder;
5079
}
5180

spring-integration-webflux/src/main/java/org/springframework/integration/webflux/config/WebFluxOutboundGatewayParser.java

+1-12
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,10 @@
1818

1919
import org.w3c.dom.Element;
2020

21-
import org.springframework.beans.factory.config.RuntimeBeanReference;
2221
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2322
import org.springframework.beans.factory.xml.ParserContext;
2423
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
2524
import org.springframework.integration.http.config.HttpOutboundGatewayParser;
26-
import org.springframework.integration.webflux.outbound.WebFluxRequestExecutingMessageHandler;
27-
import org.springframework.util.StringUtils;
2825

2926
/**
3027
* Parser for the 'outbound-gateway' element of the webflux namespace.
@@ -38,15 +35,7 @@ public class WebFluxOutboundGatewayParser extends HttpOutboundGatewayParser {
3835
@Override
3936
protected BeanDefinitionBuilder getBuilder(Element element, ParserContext parserContext) {
4037
BeanDefinitionBuilder builder =
41-
BeanDefinitionBuilder.genericBeanDefinition(WebFluxRequestExecutingMessageHandler.class);
42-
43-
String webClientRef = element.getAttribute("web-client");
44-
if (StringUtils.hasText(webClientRef)) {
45-
builder.getBeanDefinition()
46-
.getConstructorArgumentValues()
47-
.addIndexedArgumentValue(1, new RuntimeBeanReference(webClientRef));
48-
}
49-
38+
WebFluxOutboundChannelAdapterParser.buildWebFluxRequestExecutingMessageHandler(element, parserContext);
5039
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-payload-to-flux");
5140
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "body-extractor");
5241
return builder;

0 commit comments

Comments
 (0)