Add Validation to HTTP Inbound (#2978)
* Add Validation to HTTP Inbound * Pull a validation functionality from `WebFluxInboundEndpoint` to its super class `BaseHttpInboundEndpoint` making a validation available for the `HttpRequestHandlingEndpointSupport` as well * Do the same for the `validator()` option in DSL for the `HttpInboundEndpointSupportSpec` * Add `validator` XML attribute for both HTTP and WebFlux inbound endpoint XSDs * Test parsers for a new `validator` option * Document validation in the `http.adoc` * Apply some polishing in the `http.adoc`, as well as in the `HttpRequestHandlingEndpointSupport` JavaDocs * Clarify in `webflux.adoc` that validation is applied for the `Publisher` items before the payload is finally built for the message to send. * Add WebFlux into the table of endpoints in the `endpoint-summary.adoc` * * Remove unused imports * Fix `SimpleMessageListenerContainerSpec` for deprecated `txSize` option
This commit is contained in:
committed by
Gary Russell
parent
c5ec2d94c6
commit
c18c2e2141
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans
|
||||
xmlns="http://www.springframework.org/schema/integration/http"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:si="http://www.springframework.org/schema/integration"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
xmlns="http://www.springframework.org/schema/integration/http"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xmlns:si="http://www.springframework.org/schema/integration"
|
||||
xmlns:util="http://www.springframework.org/schema/util"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration
|
||||
https://www.springframework.org/schema/integration/spring-integration.xsd
|
||||
@@ -20,20 +20,25 @@
|
||||
<si:queue capacity="1"/>
|
||||
</si:channel>
|
||||
|
||||
<beans:bean id="validator" class="org.mockito.Mockito" factory-method="mock">
|
||||
<beans:constructor-arg value="org.springframework.validation.Validator"/>
|
||||
</beans:bean>
|
||||
|
||||
<inbound-channel-adapter id="defaultAdapter" channel="requests" error-channel="errorChannel"
|
||||
auto-startup="false"
|
||||
phase="1001"
|
||||
status-code-expression="'101'"/>
|
||||
auto-startup="false"
|
||||
phase="1001"
|
||||
status-code-expression="'101'"
|
||||
validator="validator"/>
|
||||
|
||||
<inbound-channel-adapter id="postOnlyAdapter" path="/postOnly" channel="requests" supported-methods="POST"/>
|
||||
|
||||
<inbound-channel-adapter id="adapterWithCustomConverterWithDefaults" message-converters="customConverters"
|
||||
channel="requests" supported-methods="DELETE" merge-with-default-converters="true"/>
|
||||
channel="requests" supported-methods="DELETE" merge-with-default-converters="true"/>
|
||||
|
||||
<inbound-channel-adapter id="adapterWithCustomConverterNoDefaults" message-converters="customConverters"
|
||||
channel="requests" supported-methods="HEAD" />
|
||||
channel="requests" supported-methods="HEAD"/>
|
||||
|
||||
<inbound-channel-adapter id="adapterNoCustomConverterNoDefaults" channel="requests" supported-methods="POST" />
|
||||
<inbound-channel-adapter id="adapterNoCustomConverterNoDefaults" channel="requests" supported-methods="POST"/>
|
||||
|
||||
<util:list id="customConverters">
|
||||
<beans:bean class="org.springframework.integration.http.converter.SerializingHttpMessageConverter"/>
|
||||
@@ -42,7 +47,7 @@
|
||||
<inbound-channel-adapter id="putOrDeleteAdapter" channel="requests" supported-methods="PUT, delete"/>
|
||||
|
||||
<inbound-channel-adapter id="inboundController" channel="requests" view-name="foo" error-code="oops"
|
||||
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED">
|
||||
status-code-expression="T(org.springframework.http.HttpStatus).ACCEPTED">
|
||||
<request-mapping headers="BAR"/>
|
||||
</inbound-channel-adapter>
|
||||
|
||||
|
||||
@@ -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<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterWithDefaults, "messageConverters", List.class);
|
||||
List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterWithCustomConverterNoDefaults, "messageConverters", List.class);
|
||||
List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> messageConverters = TestUtils.getPropertyValue(adapterNoCustomConverterNoDefaults, "messageConverters", List.class);
|
||||
List<HttpMessageConverter<?>> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user