GH-3154: Support UriBuilderFactory.EncodingMode (#3162)

* GH-3154: Support `UriBuilderFactory.EncodingMode`

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

Spring Framework now provides a `DefaultUriBuilderFactory.EncodingMode`
for encoding URIs in the `RestTemplate` before and after uri template
enrichment with uri variables.
Therefore `encodeUri` and manual uri variables substitution is not necessary
in Spring Integration HTTP components

* Deprecate `AbstractHttpRequestExecutingMessageHandler.encodeUri` in favor of
`DefaultUriBuilderFactory.EncodingMode` and respective configuration
on the `RestTemplate` in HTTP module and `WebClient` in WebFlux module

* * Really populate `uriFactory` into an internal `RestTemplate`
* Ensure in tests that `encoding-mode` is populated properly into an internal `RestTemplate`
* Clean up affected HTTP tests for AssertJ and JUnit 5

* * Clean up formatting

* * Apply fix for WebFlux module
* Add docs for new `encoding-mode` option

* * Remove unused import in the test
This commit is contained in:
Artem Bilan
2020-01-30 15:12:39 -05:00
committed by GitHub
parent 9f07803abf
commit 89d86e1904
17 changed files with 420 additions and 340 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -25,6 +25,7 @@ import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.util.StringUtils;
/**
@@ -46,7 +47,6 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
builder.addPropertyValue("expectReply", false);
HttpAdapterParsingUtils.configureUrlConstructorArg(element, parserContext, builder);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
HttpAdapterParsingUtils.setHttpMethodOrExpression(element, parserContext, builder);
String headerMapper = element.getAttribute("header-mapper");
@@ -60,8 +60,8 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
builder.addPropertyReference("headerMapper", headerMapper);
}
else if (StringUtils.hasText(mappedRequestHeaders)) {
BeanDefinitionBuilder headerMapperBuilder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.http.support.DefaultHttpHeaderMapper");
BeanDefinitionBuilder headerMapperBuilder =
BeanDefinitionBuilder.genericBeanDefinition(DefaultHttpHeaderMapper.class);
IntegrationNamespaceUtils.setValueIfAttributeDefined(headerMapperBuilder, element,
"mapped-request-headers", "outboundHeaderNames");
builder.addPropertyValue("headerMapper", headerMapperBuilder.getBeanDefinition());
@@ -90,6 +90,8 @@ public class HttpOutboundChannelAdapterParser extends AbstractOutboundChannelAda
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encoding-mode");
}
return builder;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -47,7 +47,6 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
BeanDefinitionBuilder builder = getBuilder(element, parserContext);
HttpAdapterParsingUtils.configureUrlConstructorArg(element, parserContext, builder);
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
HttpAdapterParsingUtils.setHttpMethodOrExpression(element, parserContext, builder);
String headerMapper = element.getAttribute("header-mapper");
@@ -103,6 +102,8 @@ public class HttpOutboundGatewayParser extends AbstractConsumerEndpointParser {
for (String referenceAttributeName : HttpAdapterParsingUtils.SYNC_REST_TEMPLATE_REFERENCE_ATTRIBUTES) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, referenceAttributeName);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encode-uri");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "encoding-mode");
}
return builder;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2020 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.
@@ -34,6 +34,7 @@ import org.springframework.integration.http.support.DefaultHttpHeaderMapper;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* The base {@link MessageHandlerSpec} for {@link AbstractHttpRequestExecutingMessageHandler}s.
@@ -71,12 +72,25 @@ public abstract class BaseHttpMessageHandlerSpec<S extends BaseHttpMessageHandle
* expanding and before send request via underlying implementation. The default value is <code>true</code>.
* @param encodeUri true if the URI should be encoded.
* @return the spec
* @deprecated since 5.3 in favor of {@link #encodingMode}
*/
@Deprecated
public S encodeUri(boolean encodeUri) {
this.target.setEncodeUri(encodeUri);
return _this();
}
/**
* Specify a {@link DefaultUriBuilderFactory.EncodingMode} for uri construction.
* @param encodingMode to use for uri construction.
* @return the spec
* @since 5.3
*/
public S encodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode) {
this.target.setEncodingMode(encodingMode);
return _this();
}
/**
* Specify the SpEL {@link Expression} to determine {@link HttpMethod} at runtime.
* @param httpMethodExpression The method expression.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017-2019 the original author or authors.
* Copyright 2017-2020 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.
@@ -17,7 +17,6 @@
package org.springframework.integration.http.outbound;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -27,7 +26,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Supplier;
import javax.xml.transform.Source;
@@ -56,14 +54,13 @@ 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;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.DefaultUriBuilderFactory;
import org.springframework.web.util.UriComponentsBuilder;
/**
@@ -85,6 +82,8 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
private static final List<HttpMethod> NO_BODY_HTTP_METHODS =
Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE);
protected final DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory(); // NOSONAR - final
private final Map<String, Expression> uriVariableExpressions = new HashMap<>();
private final Expression uriExpression;
@@ -95,8 +94,6 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
private boolean trustedSpel;
private boolean encodeUri = true;
private Expression httpMethodExpression = new ValueExpression<>(HttpMethod.POST);
private boolean expectReply = true;
@@ -127,9 +124,27 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
* <code>true</code>.
* @param encodeUri true if the URI should be encoded.
* @see UriComponentsBuilder
* @deprecated since 5.3 in favor of {@link #setEncodingMode}
*/
@Deprecated
public void setEncodeUri(boolean encodeUri) {
this.encodeUri = encodeUri;
setEncodingMode(
encodeUri
? DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES
: DefaultUriBuilderFactory.EncodingMode.NONE);
}
/**
* Set the encoding mode to use.
* By default this is set to {@link DefaultUriBuilderFactory.EncodingMode#TEMPLATE_AND_VALUES}.
* For more complicated scenarios consider to configure an {@link org.springframework.web.util.UriTemplateHandler}
* on an externally provided {@link org.springframework.web.client.RestTemplate}.
* @param encodingMode the mode to use for uri encoding
* @since 5.3
*/
public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode) {
Assert.notNull(encodingMode, "'encodingMode' must not be null");
this.uriFactory.setEncodingMode(encodingMode);
}
/**
@@ -291,32 +306,23 @@ public abstract class AbstractHttpRequestExecutingMessageHandler extends Abstrac
Object expectedResponseType = determineExpectedResponseType(requestMessage);
HttpEntity<?> httpRequest = generateHttpRequest(requestMessage, httpMethod);
return exchange(() -> generateUri(requestMessage), httpMethod, httpRequest, expectedResponseType,
requestMessage);
}
protected abstract Object exchange(Supplier<URI> uriSupplier, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage);
private URI generateUri(Message<?> requestMessage) {
Object uri = this.uriExpression.getValue(this.evaluationContext, requestMessage);
Assert.state(uri instanceof String || uri instanceof URI,
() -> "'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);
UriComponents uriComponents = uriComponentsBuilder.buildAndExpand(uriVariables);
try {
return this.encodeUri ? uriComponents.encode().toUri() : new URI(uriComponents.toUriString());
}
catch (URISyntaxException e) {
throw new MessageHandlingException(requestMessage, "Invalid URI [" + uri + "] in the [" + this + ']', e);
Map<String, ?> uriVariables = null;
if (uri instanceof String) {
uriVariables = determineUriVariables(requestMessage);
}
return exchange(uri, httpMethod, httpRequest, expectedResponseType, requestMessage, uriVariables);
}
protected abstract Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables);
protected Object getReply(ResponseEntity<?> httpResponse) {
if (this.expectReply) {
HttpHeaders httpHeaders = httpResponse.getHeaders();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 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.
@@ -18,7 +18,7 @@ package org.springframework.integration.http.outbound;
import java.net.URI;
import java.util.List;
import java.util.function.Supplier;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.expression.Expression;
@@ -29,12 +29,14 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.util.Assert;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* A {@link org.springframework.messaging.MessageHandler}
@@ -64,6 +66,8 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu
private final RestTemplate restTemplate;
private final boolean restTemplateExplicitlySet;
/**
* Create a handler that will send requests to the provided URI.
* @param uri The URI.
@@ -109,14 +113,24 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu
* {@link org.springframework.beans.factory.BeanFactory}.
* @param restTemplate The rest template.
*/
public HttpRequestExecutingMessageHandler(Expression uriExpression, RestTemplate restTemplate) {
public HttpRequestExecutingMessageHandler(Expression uriExpression, @Nullable RestTemplate restTemplate) {
super(uriExpression);
this.restTemplate = (restTemplate == null ? new RestTemplate() : restTemplate);
this.restTemplateExplicitlySet = restTemplate != null;
this.restTemplate = (this.restTemplateExplicitlySet ? restTemplate : new RestTemplate());
if (!this.restTemplateExplicitlySet) {
this.restTemplate.setUriTemplateHandler(this.uriFactory);
}
}
@Override
public String getComponentType() {
return (this.isExpectReply() ? "http:outbound-gateway" : "http:outbound-channel-adapter");
return (isExpectReply() ? "http:outbound-gateway" : "http:outbound-channel-adapter");
}
private void assertLocalRestTemplate(String option) {
Assert.isTrue(!this.restTemplateExplicitlySet,
() -> "The option '" + option + "' must be provided on the externally configured RestTemplate: "
+ this.restTemplate);
}
/**
@@ -125,6 +139,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu
* @see RestTemplate#setErrorHandler(ResponseErrorHandler)
*/
public void setErrorHandler(ResponseErrorHandler errorHandler) {
assertLocalRestTemplate("errorHandler");
this.restTemplate.setErrorHandler(errorHandler);
}
@@ -135,6 +150,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu
* @see RestTemplate#setMessageConverters(java.util.List)
*/
public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
assertLocalRestTemplate("messageConverters");
this.restTemplate.setMessageConverters(messageConverters);
}
@@ -144,24 +160,43 @@ public class HttpRequestExecutingMessageHandler extends AbstractHttpRequestExecu
* @see RestTemplate#setRequestFactory(ClientHttpRequestFactory)
*/
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
assertLocalRestTemplate("requestFactory");
this.restTemplate.setRequestFactory(requestFactory);
}
@Override
protected Object exchange(Supplier<URI> uriSupplier, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage) {
public void setEncodingMode(DefaultUriBuilderFactory.EncodingMode encodingMode) {
assertLocalRestTemplate("encodingMode on UriTemplateHandler");
super.setEncodingMode(encodingMode);
}
@Override
protected Object exchange(Object uri, HttpMethod httpMethod, HttpEntity<?> httpRequest,
Object expectedResponseType, Message<?> requestMessage, Map<String, ?> uriVariables) {
URI uri = uriSupplier.get();
ResponseEntity<?> httpResponse;
try {
if (expectedResponseType instanceof ParameterizedTypeReference<?>) {
httpResponse = this.restTemplate.exchange(uri, httpMethod, httpRequest,
(ParameterizedTypeReference<?>) expectedResponseType);
if (uri instanceof URI) {
if (expectedResponseType instanceof ParameterizedTypeReference<?>) {
httpResponse = this.restTemplate.exchange((URI) uri, httpMethod, httpRequest,
(ParameterizedTypeReference<?>) expectedResponseType);
}
else {
httpResponse = this.restTemplate.exchange((URI) uri, httpMethod, httpRequest,
(Class<?>) expectedResponseType);
}
}
else {
httpResponse = this.restTemplate.exchange(uri, httpMethod, httpRequest,
(Class<?>) expectedResponseType);
if (expectedResponseType instanceof ParameterizedTypeReference<?>) {
httpResponse = this.restTemplate.exchange((String) uri, httpMethod, httpRequest,
(ParameterizedTypeReference<?>) expectedResponseType, uriVariables);
}
else {
httpResponse = this.restTemplate.exchange((String) uri, httpMethod, httpRequest,
(Class<?>) expectedResponseType, uriVariables);
}
}
return getReply(httpResponse);
}
catch (RestClientException e) {

View File

@@ -844,12 +844,23 @@
<xsd:attribute name="encode-uri" type="xsd:string" default="true">
<xsd:annotation>
<xsd:documentation>
When set to "false", the real URI won't be encoded before the request is sent. This may be useful
in some scenarios as it allows user control over the encoding, if needed,
[DEPRECATED] When set to "false", the real URI won't be encoded before the request is sent.
This may be useful in some scenarios as it allows user control over the encoding, if needed,
for example by using the "url-expression". Default is "true".
Deprecated since 5.3 in favor of 'encoding-mode'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="encoding-mode" default="TEMPLATE_AND_VALUES">
<xsd:annotation>
<xsd:documentation>
Set the encoding mode during URI building.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="encodingModeEnumeration xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="http-method">
<xsd:annotation>
<xsd:documentation>
@@ -943,4 +954,13 @@
</xsd:attribute>
</xsd:attributeGroup>
<xsd:simpleType name="encodingModeEnumeration">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="TEMPLATE_AND_VALUES"/>
<xsd:enumeration value="VALUES_ONLY"/>
<xsd:enumeration value="URI_COMPONENT"/>
<xsd:enumeration value="NONE"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>