Support Validation in @MessageMapping annotated methods

Payload parameters in @MessageMapping annotated
methods can now also be validated when annotated
with a Validation annotation (@Valid, @Validated...).

A default Validator is registered by the MessageBroker
Configurer, but it is possible to provide a list of custom
validators as well.

Issue: SPR-11185
This commit is contained in:
Brian Clozel
2014-01-02 15:33:07 +01:00
parent 1c83e8653a
commit 2c8f670d5f
9 changed files with 335 additions and 29 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,12 +20,16 @@ import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import static org.junit.Assert.*;
@@ -33,6 +37,7 @@ import static org.junit.Assert.*;
* Test fixture for {@link PayloadArgumentResolver}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public class PayloadArgumentResolverTests {
@@ -41,20 +46,22 @@ public class PayloadArgumentResolverTests {
private MethodParameter param;
private MethodParameter paramNotRequired;
private MethodParameter paramWithSpelExpression;
private MethodParameter paramValidated;
@Before
public void setup() throws Exception {
MessageConverter messageConverter = new StringMessageConverter();
this.resolver = new PayloadArgumentResolver(messageConverter );
this.resolver = new PayloadArgumentResolver(new StringMessageConverter(), testValidator());
Method method = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage",
String.class, String.class, String.class);
String.class, String.class, String.class, String.class);
this.param = new MethodParameter(method , 0);
this.paramNotRequired = new MethodParameter(method , 1);
this.paramWithSpelExpression = new MethodParameter(method , 2);
this.paramValidated = new MethodParameter(method , 3);
this.paramValidated.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer());
}
@@ -82,12 +89,41 @@ public class PayloadArgumentResolverTests {
this.resolver.resolveArgument(this.paramWithSpelExpression, message);
}
@Test
public void resolveValidation() throws Exception {
Message<?> message = MessageBuilder.withPayload("ABC".getBytes()).build();
this.resolver.resolveArgument(this.paramValidated, message);
}
@Test(expected=MethodArgumentNotValidException.class)
public void resolveFailValidation() throws Exception {
Message<?> message = MessageBuilder.withPayload("".getBytes()).build();
this.resolver.resolveArgument(this.paramValidated, message);
}
private Validator testValidator() {
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
return String.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
String value = (String) target;
if (StringUtils.isEmpty(value.toString())) {
errors.reject("empty value");
}
}
};
}
@SuppressWarnings("unused")
private void handleMessage(
@Payload String param,
@Payload(required=false) String paramNotRequired,
@Payload("foo.bar") String paramWithSpelExpression) {
@Payload("foo.bar") String paramWithSpelExpression,
@Validated @Payload String validParam) {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,10 +26,8 @@ import org.springframework.context.support.StaticApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessageType;
@@ -37,6 +35,10 @@ import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import static org.junit.Assert.*;
@@ -58,6 +60,7 @@ public class SimpAnnotationMethodMessageHandlerTests {
SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel);
this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, channel, channel);
this.messageHandler.setApplicationContext(new StaticApplicationContext());
this.messageHandler.setValidator(new StringNotEmptyValidator());
this.messageHandler.afterPropertiesSet();
testController = new TestController();
@@ -142,6 +145,15 @@ public class SimpAnnotationMethodMessageHandlerTests {
assertEquals(12L, this.testController.arguments.get("id"));
}
@Test
public void validationError() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/pre/validation/payload");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
assertEquals("handleValidationException", this.testController.method);
}
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
@@ -210,6 +222,17 @@ public class SimpAnnotationMethodMessageHandlerTests {
this.method = "simpleBinding";
this.arguments.put("id", id);
}
@MessageMapping("/validation/payload")
public void payloadValidation(@Validated @Payload String payload) {
this.method = "payloadValidation";
this.arguments.put("message", payload);
}
@MessageExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationException() {
this.method = "handleValidationException";
}
}
@Controller
@@ -222,4 +245,18 @@ public class SimpAnnotationMethodMessageHandlerTests {
public void handle2() { }
}
private static class StringNotEmptyValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return String.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
String value = (String) target;
if (StringUtils.isEmpty(value.toString())) {
errors.reject("empty value");
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.messaging.simp.config;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
@@ -48,6 +49,8 @@ import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Controller;
import org.springframework.util.MimeTypeUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import static org.junit.Assert.*;
@@ -55,6 +58,7 @@ import static org.junit.Assert.*;
* Test fixture for {@link AbstractMessageBrokerConfiguration}.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public class MessageBrokerConfigurationTests {
@@ -64,6 +68,7 @@ public class MessageBrokerConfigurationTests {
private AnnotationConfigApplicationContext cxtCustomizedChannelConfig;
private AnnotationConfigApplicationContext cxtCustomizedValidator;
@Before
public void setupOnce() {
@@ -79,6 +84,10 @@ public class MessageBrokerConfigurationTests {
this.cxtCustomizedChannelConfig = new AnnotationConfigApplicationContext();
this.cxtCustomizedChannelConfig.register(CustomizedChannelConfig.class);
this.cxtCustomizedChannelConfig.refresh();
this.cxtCustomizedValidator = new AnnotationConfigApplicationContext();
this.cxtCustomizedValidator.register(ValidationConfig.class);
this.cxtCustomizedValidator.refresh();
}
@@ -271,6 +280,20 @@ public class MessageBrokerConfigurationTests {
assertEquals(MimeTypeUtils.APPLICATION_JSON, resolver.getDefaultMimeType());
}
@Test
public void defaultValidator() {
SimpAnnotationMethodMessageHandler messageHandler =
this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
assertThat(messageHandler.getValidator(),Matchers.notNullValue(Validator.class));
}
@Test
public void customValidator() {
SimpAnnotationMethodMessageHandler messageHandler =
this.cxtCustomizedValidator.getBean(SimpAnnotationMethodMessageHandler.class);
assertThat(messageHandler.getValidator(),Matchers.notNullValue(Validator.class));
assertThat(messageHandler.getValidator(),Matchers.instanceOf(Validator.class));
}
@Controller
static class TestController {
@@ -363,6 +386,24 @@ public class MessageBrokerConfigurationTests {
}
}
@Configuration
static class ValidationConfig extends TestMessageBrokerConfiguration {
@Override
public Validator getValidator() {
return new TestValidator();
}
}
private static class TestValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return false;
}
@Override
public void validate(Object target, Errors errors) {}
}
private static class TestChannel extends ExecutorSubscribableChannel {