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 db16a6555c..0175ea8adc 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 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; * Spec for a {@link SimpleMessageListenerContainer}. * * @author Gary Russell + * @author Artem Bilan + * * @since 5.0 * */ @@ -108,10 +110,22 @@ public class SimpleMessageListenerContainerSpec extends /** * @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 99ea7ae26f..4956f8a7d1 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse } 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse } 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 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse 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 45efb7073a..09fbecee14 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.inbound.RequestMapping; 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 @@ import org.springframework.web.bind.annotation.RequestMethod; * * @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 abstract class HttpInboundEndpointSupportSpec 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 abstract class HttpInboundEndpointSupportSpec expressions) { Assert.notNull(expressions, "'headerExpressions' must not be null"); @@ -139,7 +141,7 @@ public abstract class HttpInboundEndpointSupportSpec 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 abstract class HttpInboundEndpointSupportSpec, ?> 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 54ee64e9d4..815d1f6aef 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.context.OrderlyShutdownCapable; 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 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements 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 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements 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 96176455ee..a9de5dc539 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 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound 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 af797ae39e..4800232f50 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 f3361c3579..e29c6f9761 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 a6a9beff61..dc5e9a85ca 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.messaging.PollableChannel; 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 @@ import org.springframework.web.servlet.HandlerMapping; * @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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes 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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes 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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes } @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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes 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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes @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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes } @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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes @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 class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes @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 @@ public class HttpInboundChannelAdapterParserTests extends AbstractHttpInboundTes 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 c0630ff51a..95b693fb4c 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.request.SecurityMock 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.Autowired; 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.context.web.WebAppConfiguration; 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 class HttpDslTests { })); } + @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 class HttpDslTests { return channelSecurityInterceptor; } + @Bean + public Validator customValidator() { + return new TestModelValidator(); + } + } public static class HttpProxyResponseErrorHandler extends DefaultResponseErrorHandler { @@ -322,4 +363,35 @@ public class HttpDslTests { } + 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 73887f2769..9f017eeb01 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.core.ReactiveAdapterRegistry; 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 class WebFluxInboundEndpointSpec 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 0d926893c3..8e02c48887 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.ServerHttpRequest; 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 class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W 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 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W 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 @@ public class WebFluxInboundEndpoint extends BaseHttpInboundEndpoint implements W } } - 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 d47bfaaaa5..5a76ebb1d4 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 56fa9a9194..1f6012280d 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 9c704e0ef2..c9ec403d25 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 03119ca063..e8ab0d1dd7 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 a324fadd67..d6fbb6e401 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