diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/dsl/SimpleMessageListenerContainerSpec.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/dsl/SimpleMessageListenerContainerSpec.java index db16a6555c5..0175ea8adc5 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/dsl/SimpleMessageListenerContainerSpec.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/dsl/SimpleMessageListenerContainerSpec.java @@ -22,6 +22,8 @@ * Spec for a {@link SimpleMessageListenerContainer}. * * @author Gary Russell + * @author Artem Bilan + * * @since 5.0 * */ @@ -108,10 +110,22 @@ public SimpleMessageListenerContainerSpec receiveTimeout(long receiveTimeout) { /** * @param txSize the txSize. * @return the spec. - * @see SimpleMessageListenerContainer#setTxSize(int) + * @see SimpleMessageListenerContainer#setBatchSize(int) + * @deprecated since 5.2 in favor of {@link #batchSize(int)} */ public SimpleMessageListenerContainerSpec txSize(int txSize) { - this.listenerContainer.setTxSize(txSize); + return batchSize(txSize); + } + + /** + * The batch size to use. + * @param batchSize the batchSize. + * @return the spec. + * @see SimpleMessageListenerContainer#setBatchSize(int) + * @since 5.2 + */ + public SimpleMessageListenerContainerSpec batchSize(int batchSize) { + this.listenerContainer.setBatchSize(batchSize); return this; } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java index 99ea7ae26fb..4956f8a7d12 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java @@ -107,12 +107,13 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit List headerElements = DomUtils.getChildElementsByTagName(element, "header"); if (!CollectionUtils.isEmpty(headerElements)) { - ManagedMap headerElementsMap = new ManagedMap(); + ManagedMap headerElementsMap = new ManagedMap<>(); for (Element headerElement : headerElements) { String name = headerElement.getAttribute(NAME_ATTRIBUTE); BeanDefinition headerExpressionDef = - IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined(IntegrationNamespaceUtils.EXPRESSION_ATTRIBUTE, - headerElement); + IntegrationNamespaceUtils + .createExpressionDefIfAttributeDefined(IntegrationNamespaceUtils.EXPRESSION_ATTRIBUTE, + headerElement); if (headerExpressionDef != null) { headerElementsMap.put(name, headerExpressionDef); } @@ -133,8 +134,8 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit } BeanDefinition expressionDef = - IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("view-name", "view-expression", - parserContext, element, false); + IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("view-name", + "view-expression", parserContext, element, false); if (expressionDef != null) { builder.addPropertyValue("viewExpression", expressionDef); } @@ -154,13 +155,16 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit if (StringUtils.hasText(headerMapper)) { if (hasMappedRequestHeaders || hasMappedResponseHeaders) { - parserContext.getReaderContext().error("Neither 'mapped-request-headers' or 'mapped-response-headers' " + - "attributes are allowed when a 'header-mapper' has been specified.", parserContext.extractSource(element)); + parserContext.getReaderContext() + .error("Neither 'mapped-request-headers' or 'mapped-response-headers' " + + "attributes are allowed when a 'header-mapper' has been specified.", + parserContext.extractSource(element)); } builder.addPropertyReference("headerMapper", headerMapper); } else { - BeanDefinitionBuilder headerMapperBuilder = BeanDefinitionBuilder.genericBeanDefinition(DefaultHttpHeaderMapper.class); + BeanDefinitionBuilder headerMapperBuilder = + BeanDefinitionBuilder.genericBeanDefinition(DefaultHttpHeaderMapper.class); headerMapperBuilder.setFactoryMethod("inboundMapper"); if (hasMappedRequestHeaders) { @@ -181,7 +185,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit if (crossOriginElement != null) { BeanDefinitionBuilder crossOriginBuilder = BeanDefinitionBuilder.genericBeanDefinition(CrossOrigin.class); - String[] attributes = {"origin", "allowed-headers", "exposed-headers", "max-age", "method"}; + String[] attributes = { "origin", "allowed-headers", "exposed-headers", "max-age", "method" }; for (String crossOriginAttribute : attributes) { IntegrationNamespaceUtils.setValueIfAttributeDefined(crossOriginBuilder, crossOriginElement, crossOriginAttribute); @@ -191,7 +195,8 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit builder.addPropertyValue("crossOrigin", crossOriginBuilder.getBeanDefinition()); } - IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type", "requestPayloadTypeClass"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, + "request-payload-type", "requestPayloadTypeClass"); BeanDefinition statusCodeExpressionDef = IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("status-code-expression", element); @@ -205,6 +210,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.AUTO_STARTUP); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, IntegrationNamespaceUtils.PHASE); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "validator"); } private String getInputChannelAttributeName() { @@ -212,7 +218,8 @@ private String getInputChannelAttributeName() { } private BeanDefinition createRequestMapping(Element element) { - BeanDefinitionBuilder requestMappingDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(RequestMapping.class); + BeanDefinitionBuilder requestMappingDefBuilder = + BeanDefinitionBuilder.genericBeanDefinition(RequestMapping.class); String methods = element.getAttribute("supported-methods"); if (StringUtils.hasText(methods)) { @@ -224,7 +231,7 @@ private BeanDefinition createRequestMapping(Element element) { Element requestMappingElement = DomUtils.getChildElementByTagName(element, "request-mapping"); if (requestMappingElement != null) { - for (String requestMappingAttribute : new String[]{"params", "headers", "consumes", "produces"}) { + for (String requestMappingAttribute : new String[] { "params", "headers", "consumes", "produces" }) { IntegrationNamespaceUtils.setValueIfAttributeDefined(requestMappingDefBuilder, requestMappingElement, requestMappingAttribute); } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/dsl/HttpInboundEndpointSupportSpec.java b/spring-integration-http/src/main/java/org/springframework/integration/http/dsl/HttpInboundEndpointSupportSpec.java index 45efb7073ad..09fbecee14c 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/dsl/HttpInboundEndpointSupportSpec.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/dsl/HttpInboundEndpointSupportSpec.java @@ -37,6 +37,7 @@ import org.springframework.integration.http.support.DefaultHttpHeaderMapper; import org.springframework.integration.mapping.HeaderMapper; import org.springframework.util.Assert; +import org.springframework.validation.Validator; import org.springframework.web.bind.annotation.RequestMethod; /** @@ -45,7 +46,8 @@ * * @since 5.0 */ -public abstract class HttpInboundEndpointSupportSpec, E extends BaseHttpInboundEndpoint> +public abstract class HttpInboundEndpointSupportSpec, + E extends BaseHttpInboundEndpoint> extends MessagingGatewaySpec implements ComponentsRegistration { @@ -93,7 +95,7 @@ public S crossOrigin(Consumer crossOrigin) { * Specify a SpEL expression to evaluate in order to generate the Message payload. * @param payloadExpression The payload expression. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression) + * @see BaseHttpInboundEndpoint#setPayloadExpression(Expression) */ public S payloadExpression(String payloadExpression) { return payloadExpression(PARSER.parseExpression(payloadExpression)); @@ -103,7 +105,7 @@ public S payloadExpression(String payloadExpression) { * Specify a SpEL expression to evaluate in order to generate the Message payload. * @param payloadExpression The payload expression. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression) + * @see BaseHttpInboundEndpoint#setPayloadExpression(Expression) */ public S payloadExpression(Expression payloadExpression) { this.target.setPayloadExpression(payloadExpression); @@ -115,7 +117,7 @@ public S payloadExpression(Expression payloadExpression) { * @param payloadFunction The payload {@link Function}. * @param

the expected HTTP request body type. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setPayloadExpression(Expression) + * @see BaseHttpInboundEndpoint#setPayloadExpression(Expression) */ public

S payloadFunction(Function, ?> payloadFunction) { return payloadExpression(new FunctionExpression<>(payloadFunction)); @@ -125,7 +127,7 @@ public

S payloadFunction(Function, ?> payloadFunction) { * Specify a Map of SpEL expressions to evaluate in order to generate the Message headers. * @param expressions The {@link Map} of SpEL expressions for headers. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map) + * @see BaseHttpInboundEndpoint#setHeaderExpressions(Map) */ public S headerExpressions(Map expressions) { Assert.notNull(expressions, "'headerExpressions' must not be null"); @@ -139,7 +141,7 @@ public S headerExpressions(Map expressions) { * @param header the header name to populate. * @param expression the SpEL expression for the header. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map) + * @see BaseHttpInboundEndpoint#setHeaderExpressions(Map) */ public S headerExpression(String header, String expression) { return headerExpression(header, PARSER.parseExpression(expression)); @@ -150,7 +152,7 @@ public S headerExpression(String header, String expression) { * @param header the header name to populate. * @param expression the SpEL expression for the header. * @return the spec - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map) + * @see BaseHttpInboundEndpoint#setHeaderExpressions(Map) */ public S headerExpression(String header, Expression expression) { this.headerExpressions.put(header, expression); @@ -163,7 +165,7 @@ public S headerExpression(String header, Expression expression) { * @param headerFunction the function to evaluate the header value against {@link HttpEntity}. * @param

the expected HTTP body type. * @return the current Spec. - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setHeaderExpressions(Map) + * @see BaseHttpInboundEndpoint#setHeaderExpressions(Map) */ public

S headerFunction(String header, Function, ?> headerFunction) { return headerExpression(header, new FunctionExpression<>(headerFunction)); @@ -251,7 +253,7 @@ public S extractReplyPayload(boolean extractReplyPayload) { * the default '200 OK' or '500 Internal Server Error' for a timeout. * @param statusCodeExpression The status code Expression. * @return the current Spec. - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression) + * @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression) */ public S statusCodeExpression(String statusCodeExpression) { this.target.setStatusCodeExpressionString(statusCodeExpression); @@ -263,7 +265,7 @@ public S statusCodeExpression(String statusCodeExpression) { * the default '200 OK' or '500 Internal Server Error' for a timeout. * @param statusCodeExpression The status code Expression. * @return the current Spec. - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression) + * @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression) */ public S statusCodeExpression(Expression statusCodeExpression) { this.target.setStatusCodeExpression(statusCodeExpression); @@ -275,12 +277,23 @@ public S statusCodeExpression(Expression statusCodeExpression) { * the default '200 OK' or '500 Internal Server Error' for a timeout. * @param statusCodeFunction The status code {@link Function}. * @return the current Spec. - * @see org.springframework.integration.http.inbound.HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression) + * @see BaseHttpInboundEndpoint#setStatusCodeExpression(Expression) */ public S statusCodeFunction(Function, ?> statusCodeFunction) { return statusCodeExpression(new FunctionExpression<>(statusCodeFunction)); } + /** + * Specify a {@link Validator} to validate a converted payload from request. + * @param validator the {@link Validator} to use. + * @return the spec + * @since 5.2 + */ + public S validator(Validator validator) { + this.target.setValidator(validator); + return _this(); + } + @Override public Map getComponentsToRegister() { HeaderMapper headerMapperToRegister = diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/BaseHttpInboundEndpoint.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/BaseHttpInboundEndpoint.java index 54ee64e9d4b..815d1f6aef8 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/BaseHttpInboundEndpoint.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/BaseHttpInboundEndpoint.java @@ -33,11 +33,15 @@ import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.gateway.MessagingGatewaySupport; import org.springframework.integration.http.support.DefaultHttpHeaderMapper; +import org.springframework.integration.http.support.IntegrationWebExchangeBindException; import org.springframework.integration.mapping.HeaderMapper; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; /** * The {@link MessagingGatewaySupport} extension for HTTP Inbound endpoints @@ -65,6 +69,8 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements private final boolean expectReply; + private Validator validator; + private ResolvableType requestPayloadType = null; private HeaderMapper headerMapper = DefaultHttpHeaderMapper.inboundMapper(); @@ -253,6 +259,19 @@ protected Expression getStatusCodeExpression() { return this.statusCodeExpression; } + /** + * Specify a {@link Validator} to validate a converted payload from request. + * @param validator the {@link Validator} to use. + * @since 5.2 + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + protected Validator getValidator() { + return this.validator; + } + @Override protected void onInit() { super.onInit(); @@ -334,4 +353,12 @@ protected boolean isReadable(HttpMethod httpMethod) { return !(CollectionUtils.containsInstance(NON_READABLE_BODY_HTTP_METHODS, httpMethod)); } + protected void validate(Object value) { + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(value, "requestPayload"); + ValidationUtils.invokeValidator(this.validator, value, errors); + if (errors.hasErrors()) { + throw new IntegrationWebExchangeBindException(getComponentName(), value, errors); + } + } + } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java index 96176455ee7..a9de5dc5394 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java @@ -319,6 +319,10 @@ private Message prepareRequestMessage(HttpServletRequest servletRequest, Requ AbstractIntegrationMessageBuilder messageBuilder; + if (getValidator() != null) { + validate(payload); + } + if (payload instanceof Message) { messageBuilder = getMessageBuilderFactory() diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.2.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.2.xsd index af797ae39e6..4800232f504 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.2.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-5.2.xsd @@ -362,6 +362,18 @@ + + + + + + + + + A 'Validator' bean reference to validate a payload converted from the HTTP request. + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml index f3361c35794..e29c6f9761b 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml @@ -1,11 +1,11 @@ + + + + + auto-startup="false" + phase="1001" + status-code-expression="'101'" + validator="validator"/> + channel="requests" supported-methods="DELETE" merge-with-default-converters="true"/> + channel="requests" supported-methods="HEAD"/> - + @@ -42,7 +47,7 @@ + status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED"> diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java index a6a9beff611..dc5e9a85cac 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java @@ -17,6 +17,9 @@ package org.springframework.integration.http.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willReturn; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; @@ -49,10 +52,10 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.AntPathMatcher; import org.springframework.util.MultiValueMap; +import org.springframework.validation.Validator; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.servlet.HandlerMapping; @@ -65,8 +68,7 @@ * @author Artem Bilan * @author Biju Kunjummen */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration +@RunWith(SpringRunner.class) @DirtiesContext public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTests { @@ -112,9 +114,13 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes @Autowired private MessageChannel autoChannel; - @Autowired @Qualifier("autoChannel.adapter") + @Autowired + @Qualifier("autoChannel.adapter") private HttpRequestHandlingMessagingGateway autoChannelAdapter; + @Autowired + private Validator validator; + @Test @SuppressWarnings("unchecked") public void getRequestOk() throws Exception { @@ -128,6 +134,7 @@ public void getRequestOk() throws Exception { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_SERVICE_UNAVAILABLE); this.defaultAdapter.start(); response = new MockHttpServletResponse(); + willReturn(true).given(this.validator).supports(any()); this.defaultAdapter.handleRequest(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_SWITCHING_PROTOCOLS); Message message = requests.receive(0); @@ -140,12 +147,13 @@ public void getRequestOk() throws Exception { assertThat(map.get("foo").size()).isEqualTo(1); assertThat(map.getFirst("foo")).isEqualTo("bar"); assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "errorChannel")).isNotNull(); + assertThat(TestUtils.getPropertyValue(this.defaultAdapter, "validator")).isSameAs(this.validator); } @Test - public void getRequestWithHeaders() throws Exception { + public void getRequestWithHeaders() { DefaultHttpHeaderMapper headerMapper = - (DefaultHttpHeaderMapper) TestUtils.getPropertyValue(withMappedHeaders, "headerMapper"); + TestUtils.getPropertyValue(withMappedHeaders, "headerMapper", DefaultHttpHeaderMapper.class); HttpHeaders headers = new HttpHeaders(); headers.set("foo", "foo"); @@ -187,19 +195,18 @@ public void withExpressions() throws Exception { } @Test - public void getRequestNotAllowed() throws Exception { + public void getRequestNotAllowed() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); request.setParameter("foo", "bar"); request.setRequestURI("/postOnly"); - try { - this.integrationRequestMappingHandlerMapping.getHandler(request); - } - catch (HttpRequestMethodNotSupportedException e) { - assertThat(e.getMethod()).isEqualTo("GET"); - assertThat(e.getSupportedMethods()).isEqualTo(new String[] { "POST" }); - } + assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class) + .isThrownBy(() -> this.integrationRequestMappingHandlerMapping.getHandler(request)) + .satisfies((ex) -> { + assertThat(ex.getMethod()).isEqualTo("GET"); + assertThat(ex.getSupportedMethods()).containsExactly("POST"); + }); } @Test @@ -222,7 +229,8 @@ public void postRequestWithTextContentOk() throws Exception { assertThat(message.getPayload()).isEqualTo("test"); } - @Test @DirtiesContext + @Test + @DirtiesContext public void postRequestWithSerializedObjectContentOk() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("POST"); @@ -244,15 +252,15 @@ public void postRequestWithSerializedObjectContentOk() throws Exception { @Test - public void putOrDeleteMethodsSupported() throws Exception { + public void putOrDeleteMethodsSupported() { HttpMethod[] supportedMethods = TestUtils.getPropertyValue(putOrDeleteAdapter, "requestMapping.methods", HttpMethod[].class); assertThat(supportedMethods.length).isEqualTo(2); - assertThat(supportedMethods).isEqualTo(new HttpMethod[] { HttpMethod.PUT, HttpMethod.DELETE }); + assertThat(supportedMethods).containsExactly(HttpMethod.PUT, HttpMethod.DELETE); } @Test - public void testController() throws Exception { + public void testController() { String errorCode = TestUtils.getPropertyValue(inboundController, "errorCode", String.class); assertThat(errorCode).isEqualTo("oops"); Expression viewExpression = TestUtils.getPropertyValue(inboundController, "viewExpression", Expression.class); @@ -269,8 +277,9 @@ public void testController() throws Exception { } @Test - public void testInt2717ControllerWithViewExpression() throws Exception { - Expression viewExpression = TestUtils.getPropertyValue(inboundControllerViewExp, "viewExpression", Expression.class); + public void testInt2717ControllerWithViewExpression() { + Expression viewExpression = TestUtils + .getPropertyValue(inboundControllerViewExp, "viewExpression", Expression.class); assertThat(viewExpression.getExpressionString()).isEqualTo("'foo'"); } @@ -282,7 +291,8 @@ public void testAutoChannel() { @Test public void testInboundAdapterWithMessageConverterDefaults() { @SuppressWarnings("unchecked") - List> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterWithDefaults, "messageConverters", List.class); + List> messageConverters = TestUtils + .getPropertyValue(adapterWithCustomConverterWithDefaults, "messageConverters", List.class); assertThat(messageConverters.size() > 1) .as("There should be more than 1 message converter. The customized one and the defaults.").isTrue(); @@ -293,17 +303,19 @@ public void testInboundAdapterWithMessageConverterDefaults() { @Test public void testInboundAdapterWithNoMessageConverterDefaults() { @SuppressWarnings("unchecked") - List> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterNoDefaults, "messageConverters", List.class); + List> messageConverters = TestUtils + .getPropertyValue(adapterWithCustomConverterNoDefaults, "messageConverters", List.class); //First converter should be the customized one assertThat(messageConverters.get(0)).isInstanceOf(SerializingHttpMessageConverter.class); - assertThat(messageConverters.size() == 1).as("There should be only the customized messageconverter registered.") - .isTrue(); + assertThat(messageConverters).as("There should be only the customized MessageConverter registered.") + .hasSize(1); } @Test public void testInboundAdapterWithNoMessageConverterNoDefaults() { @SuppressWarnings("unchecked") - List> messageConverters = TestUtils.getPropertyValue(adapterNoCustomConverterNoDefaults, "messageConverters", List.class); + List> messageConverters = TestUtils + .getPropertyValue(adapterNoCustomConverterNoDefaults, "messageConverters", List.class); assertThat(messageConverters.size() > 1).as("There should be more than 1 message converter. The defaults.") .isTrue(); } @@ -316,6 +328,7 @@ private static class TestObject implements Serializable { TestObject(String text) { this.text = text; } + } } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java index c0630ff51a8..95b693fb4c5 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java @@ -21,6 +21,7 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -38,6 +39,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestFactory; @@ -71,6 +73,9 @@ import org.springframework.test.web.client.MockMvcClientHttpRequestFactory; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.context.WebApplicationContext; @@ -199,6 +204,37 @@ public void testMultiPartFiles() throws Exception { })); } + @Autowired + private Validator validator; + + @Test + public void testValidation() throws Exception { + IntegrationFlow flow = + IntegrationFlows.from( + Http.inboundChannelAdapter("/validation") + .requestMapping((mapping) -> mapping + .methods(HttpMethod.POST) + .consumes(MediaType.APPLICATION_JSON_VALUE)) + .requestPayloadType(TestModel.class) + .validator(this.validator)) + .bridge() + .get(); + + IntegrationFlowContext.IntegrationFlowRegistration flowRegistration = + this.integrationFlowContext.registration(flow).register(); + + this.mockMvc.perform( + post("/validation") + .with(httpBasic("user", "user")) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\": \"\"}")) + .andExpect(status().isBadRequest()) + .andExpect(status().reason("Validation failure")); + + flowRegistration.destroy(); + } + + @Configuration @EnableWebSecurity @EnableIntegration @@ -309,6 +345,11 @@ public ChannelSecurityInterceptor channelSecurityInterceptor(AccessDecisionManag return channelSecurityInterceptor; } + @Bean + public Validator customValidator() { + return new TestModelValidator(); + } + } public static class HttpProxyResponseErrorHandler extends DefaultResponseErrorHandler { @@ -322,4 +363,35 @@ protected byte[] getResponseBody(ClientHttpResponse response) { } + public static class TestModel { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + private static class TestModelValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return TestModel.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + TestModel testModel = (TestModel) target; + if (!StringUtils.hasText(testModel.getName())) { + errors.rejectValue("name", "Must not be empty"); + } + } + + } + } diff --git a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/dsl/WebFluxInboundEndpointSpec.java b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/dsl/WebFluxInboundEndpointSpec.java index 73887f27699..9f017eeb014 100644 --- a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/dsl/WebFluxInboundEndpointSpec.java +++ b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/dsl/WebFluxInboundEndpointSpec.java @@ -20,7 +20,6 @@ import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.integration.http.dsl.HttpInboundEndpointSupportSpec; import org.springframework.integration.webflux.inbound.WebFluxInboundEndpoint; -import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; /** @@ -54,15 +53,4 @@ public WebFluxInboundEndpointSpec reactiveAdapterRegistry(ReactiveAdapterRegistr return this; } - /** - * Specify a {@link Validator} to validate a converted payload from request. - * @param validator the {@link Validator} to use. - * @return the spec - * @since 5.2 - */ - public WebFluxInboundEndpointSpec validator(Validator validator) { - this.target.setValidator(validator); - return this; - } - } diff --git a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/inbound/WebFluxInboundEndpoint.java b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/inbound/WebFluxInboundEndpoint.java index 0d926893c35..8e02c488879 100644 --- a/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/inbound/WebFluxInboundEndpoint.java +++ b/spring-integration-webflux/src/main/java/org/springframework/integration/webflux/inbound/WebFluxInboundEndpoint.java @@ -48,15 +48,11 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.integration.expression.ExpressionEvalMap; import org.springframework.integration.http.inbound.BaseHttpInboundEndpoint; -import org.springframework.integration.http.support.IntegrationWebExchangeBindException; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; -import org.springframework.validation.BeanPropertyBindingResult; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; @@ -94,8 +90,6 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W private ReactiveAdapterRegistry adapterRegistry = new ReactiveAdapterRegistry(); - private Validator validator; - public WebFluxInboundEndpoint() { this(true); } @@ -133,15 +127,6 @@ public void setReactiveAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) this.adapterRegistry = adapterRegistry; } - /** - * Specify a {@link Validator} to validate a converted payload from request. - * @param validator the {@link Validator} to use. - * @since 5.2 - */ - public void setValidator(Validator validator) { - this.validator = validator; - } - @Override public String getComponentType() { return super.getComponentType().replaceFirst("http", "webflux"); @@ -244,14 +229,14 @@ private Mono readRequestBody(ServerWebExchange exchange, MediaType contentTyp Map readHints = Collections.emptyMap(); if (adapter != null && adapter.isMultiValue()) { Flux flux = httpMessageReader.read(bodyType, elementType, request, response, readHints); - if (this.validator != null) { + if (getValidator() != null) { flux = flux.doOnNext(this::validate); } return Mono.just(adapter.fromPublisher(flux)); } else { Mono mono = httpMessageReader.readMono(bodyType, elementType, request, response, readHints); - if (this.validator != null) { + if (getValidator() != null) { mono = mono.doOnNext(this::validate); } if (adapter != null) { @@ -263,14 +248,6 @@ private Mono readRequestBody(ServerWebExchange exchange, MediaType contentTyp } } - private void validate(Object value) { - BeanPropertyBindingResult errors = new BeanPropertyBindingResult(value, "requestPayload"); - ValidationUtils.invokeValidator(this.validator, value, errors); - if (errors.hasErrors()) { - throw new IntegrationWebExchangeBindException(getComponentName(), value, errors); - } - } - private Mono, RequestEntity>> buildMessage(RequestEntity httpEntity, ServerWebExchange exchange) { diff --git a/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.2.xsd b/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.2.xsd index d47bfaaaa5b..5a76ebb1d47 100644 --- a/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.2.xsd +++ b/spring-integration-webflux/src/main/resources/org/springframework/integration/webflux/config/spring-integration-webflux-5.2.xsd @@ -327,6 +327,18 @@ + + + + + + + + + A 'Validator' bean reference to validate a payload converted from the HTTP request. + + + diff --git a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxInboundChannelAdapterParserTests-context.xml b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxInboundChannelAdapterParserTests-context.xml index 56fa9a91948..1f6012280d2 100644 --- a/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-webflux/src/test/java/org/springframework/integration/webflux/config/WebFluxInboundChannelAdapterParserTests-context.xml @@ -10,7 +10,11 @@ - + + + + + > | <<./http.adoc#http-outbound,HTTP Outbound Components>> +| *WebFlux* +| <<./webflux.adoc#webflux-namespace,WebFlux Namespace Support>> +| <<./webflux.adoc#webflux-namespace,WebFlux Namespace Support>> +| <<./webflux.adoc#webflux-inbound,WebFlux Inbound Components>> +| <<./webflux.adoc#webflux-outbound,WebFlux Outbound Components>> + | *JDBC* | <<./jdbc.adoc#jdbc-inbound-channel-adapter,Inbound Channel Adapter>> and <<./jdbc.adoc#stored-procedure-inbound-channel-adapter,Stored Procedure Inbound Channel Adapter>> | <<./jdbc.adoc#jdbc-outbound-channel-adapter,Outbound Channel Adapter>> and <<./jdbc.adoc#stored-procedure-outbound-channel-adapter,Stored Procedure Outbound Channel Adapter>> diff --git a/src/reference/asciidoc/http.adoc b/src/reference/asciidoc/http.adoc index 9c704e0ef2a..c9ec403d25a 100644 --- a/src/reference/asciidoc/http.adoc +++ b/src/reference/asciidoc/http.adoc @@ -32,8 +32,7 @@ The `javax.servlet:javax.servlet-api` dependency must be provided on the target To receive messages over HTTP, you need to use an HTTP inbound channel adapter or an HTTP inbound gateway. To support the HTTP inbound adapters, they need to be deployed within a servlet container such as https://tomcat.apache.org/[Apache Tomcat] or https://www.eclipse.org/jetty/[Jetty]. -The easiest way to do this is to use Spring's -https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/support/HttpRequestHandlerServlet.html[`HttpRequestHandlerServlet`], by providing the following servlet definition in the `web.xml` file: +The easiest way to do this is to use Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/context/support/HttpRequestHandlerServlet.html[`HttpRequestHandlerServlet`], by providing the following servlet definition in the `web.xml` file: ==== [source,xml] @@ -46,9 +45,7 @@ https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/c ==== Notice that the servlet name matches the bean name. -For more information on using the `HttpRequestHandlerServlet`, see -https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remoting and web services using Spring], -which is part of the Spring Framework Reference documentation. +For more information on using the `HttpRequestHandlerServlet`, see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remoting and web services using Spring], which is part of the Spring Framework Reference documentation. If you are running within a Spring MVC application, then the aforementioned explicit servlet definition is not necessary. In that case, the bean name for your gateway can be matched against the URL path as you would for a Spring MVC Controller bean. @@ -79,8 +76,7 @@ An additional flag (`mergeWithDefaultConverters`) can be set along with the list By default, this flag is set to `false`, meaning that the custom converters replace the default list. The message conversion process uses the (optional) `requestPayloadType` property and the incoming `Content-Type` header. -Starting with version 4.3, if a request has no content type header, `application/octet-stream` is assumed, as -recommended by `RFC 2616`. +Starting with version 4.3, if a request has no content type header, `application/octet-stream` is assumed, as recommended by `RFC 2616`. Previously, the body of such messages was ignored. Spring Integration 2.0 implemented multipart file support. @@ -146,10 +142,17 @@ The preceding example also shows how to customize the HTTP methods accepted by t The reply message is available in the model map. By default, the key for that map entry is 'reply', but you can override this default by setting the 'replyKey' property on the endpoint's configuration. +[[http-validation]] +==== Payload Validation + +Starting with version 5.2, the HTTP inbound endpoints can be supplied with a `Validator` to check a payload before sending into the channel. +This payload is already a result of conversion and extraction after `payloadExpression` to narrow a validation scope in regards to the valuable data. +The validation failure handling is fully the same what we have in Spring MVC https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-exceptionhandlers[Error Handling]. + [[http-outbound]] === HTTP Outbound Components -This section describes Spring Integration's HTTP outbound components +This section describes Spring Integration's HTTP outbound components. ==== Using `HttpRequestExecutingMessageHandler` @@ -205,8 +208,7 @@ If `transfer-cookies` is `false`, any `Set-Cookie` header received remains as `S HTTP is a request-response protocol. However, the response may not have a body, only headers. In this case, the `HttpRequestExecutingMessageHandler` produces a reply `Message` with the payload being an `org.springframework.http.ResponseEntity`, regardless of any provided `expected-response-type`. -According to the https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[HTTP RFC Status Code Definitions], there are many statuses that mandate that a response must not contain a message-body (for example, -`204 No Content`). +According to the https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html[HTTP RFC Status Code Definitions], there are many statuses that mandate that a response must not contain a message-body (for example, `204 No Content`). There are also cases where calls to the same URL might or might not return a response body. For example, the first request to an HTTP resource returns content, but the second does not (returning a `304 Not Modified`). In all cases, however, the `http_statusCode` message header is populated. @@ -350,8 +352,7 @@ For more information regarding handler mappings, see https://docs.spring.io/spri [[http-cors]] ==== Cross-origin Resource Sharing (CORS) Support -Starting with version 4.2, you can configure the `` and `` with -a `` element. +Starting with version 4.2, you can configure the `` and `` with a `` element. It represents the same options as Spring MVC's `@CrossOrigin` for `@Controller` annotations and allows the configuration of cross-origin resource sharing (CORS) for Spring Integration HTTP endpoints: * `origin`: List of allowed origins. @@ -376,8 +377,7 @@ This property controls the value of the `Access-Control-Max-Age` header in the p A value of `-1` means undefined. The default value is 1800 seconds (30 minutes). -The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class, -instances of which can be injected into the `HttpRequestHandlingEndpointSupport` beans. +The CORS Java Configuration is represented by the `org.springframework.integration.http.inbound.CrossOrigin` class, instances of which can be injected into the `HttpRequestHandlingEndpointSupport` beans. [[http-response-statuscode]] ==== Response Status Code @@ -403,8 +403,7 @@ The following example shows how to set the status code to `ACCEPTED`: ==== The `` resolves the 'status code' from the `http_statusCode` header of the reply `Message`. -Starting with version 4.2, the default response status code when no reply is received within the `reply-timeout` -is `500 Internal Server Error`. +Starting with version 4.2, the default response status code when no reply is received within the `reply-timeout` is `500 Internal Server Error`. There are two ways to modify this behavior: * Add a `reply-timeout-status-code-expression`. @@ -427,8 +426,7 @@ The payload of the `ErrorMessage` is a `MessageTimeoutException`. It must be transformed to something that can be converted by the gateway, such as a `String`. A good candidate is the exception's message property, which is the value used when you use the `expression` technique. -If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or, if the -`reply-timeout-status-code-expression` is present, it is evaluated. +If the error flow times out after a main flow timeout, `500 Internal Server Error` is returned, or, if the `reply-timeout-status-code-expression` is present, it is evaluated. NOTE: Previously, the default status code for a timeout was `200 OK`. To restore that behavior, set `reply-timeout-status-code-expression="200"`. @@ -655,7 +653,7 @@ The `uriVariablesExpression` property provides a very powerful mechanism for eva We anticipate that people mostly use simple expressions, such as the preceding example. However, you can also configure something such as `"@uriVariablesBean.populate(#root)"` with an expression in the returned map being `variables.put("thing1", EXPRESSION_PARSER.parseExpression(message.getHeaders().get("thing2", String.class)));`, where the expression is dynamically provided in the message header named `thing2`. Since the header may come from an untrusted source, the HTTP outbound endpoints use `SimpleEvaluationContext` when evaluating these expressions. -`SimpleEvaluationContext` uses only a subset of SpEL features. +The `SimpleEvaluationContext` uses only a subset of SpEL features. If you trust your message sources and wish to use the restricted SpEL constructs, set the `trustedSpel` property of the outbound endpoint to `true`. ==== @@ -830,8 +828,7 @@ image::images/http-outbound-gateway.png[align="center"] //TODO These images are too small, and the text within them is much too small. You may want to configure the HTTP related timeout behavior, when making active HTTP requests by using the HTTP outbound gateway or the HTTP outbound channel adapter. -In those instances, these two components use Spring's -https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html[`RestTemplate`] support to execute HTTP requests. +In those instances, these two components use Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html[`RestTemplate`] support to execute HTTP requests. To configure timeouts for the HTTP outbound gateway and the HTTP outbound channel adapter, you can either reference a `RestTemplate` bean directly (by using the `rest-template` attribute) or you can provide a reference to a https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/client/ClientHttpRequestFactory.html[`ClientHttpRequestFactory`] bean (by using the `request-factory` attribute). Spring provides the following implementations of the `ClientHttpRequestFactory` interface: diff --git a/src/reference/asciidoc/webflux.adoc b/src/reference/asciidoc/webflux.adoc index 03119ca0632..e8ab0d1dd75 100644 --- a/src/reference/asciidoc/webflux.adoc +++ b/src/reference/asciidoc/webflux.adoc @@ -91,8 +91,14 @@ See <<./http.adoc#http-request-mapping,Request Mapping Support>> and <<./http.ad When the request body is empty or `payloadExpression` returns `null`, the request params (`MultiValueMap`) is used for a `payload` of the target message to process. +[[webflux-validation]] +==== Payload Validation + Starting with version 5.2, the `WebFluxInboundEndpoint` can be configured with a `Validator`. -It is used to validate elements in the `Publisher` to which a request has been converted by the `HttpMessageReader`. +Unlike the MVC validation in the <<./http.adoc#http-validation,HTTP Support>>, it is used to validate elements in the `Publisher` to which a request has been converted by the `HttpMessageReader`, before performing a fallback and `payloadExpression` functions. +The Framework can't assume how complex the `Publisher` object can be after building the final payload. +If there is a requirements to restrict validation visibility for exactly final payload (or its `Publisher` elements), the validation should go downstream instead of WebFlux endpoint. +See more information in the Spring WebFlux https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web-reactive.html#webflux-fn-handler-validation[documentation]. An invalid payload is rejected with an `IntegrationWebExchangeBindException` (a `WebExchangeBindException` extension), containing all the validation `Errors`. See more in Spring Framework https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation[Reference Manual] about validation. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index a324fadd678..d6fbb6e4014 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -95,6 +95,12 @@ See <<./ip.adoc#tcp-connection-factory, TCP Connection Factories>> for more info The `AbstractMailReceiver` has now an `autoCloseFolder` option (`true` by default), to disable an automatic folder close after a fetch, but populate `IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE` header instead for downstream interaction. See <<./mail.adoc#mail-inbound,Mail-receiving Channel Adapter>> for more information. +[[x5.2-http]] +==== HTTP Changes + +The HTTP inbound endpoint now support a request payload validation. +See <<./http.adoc#http,HTTP Support>> for more information. + [[x5.2-webflux]] ==== WebFlux Changes