Reputation: 43
I'm stuggling with getting only one error message per field. I have a lot of rules for each field and I want to validate them one by one. If one fails, validation stops and returns only one message describing failed rule for this field.
After research I've found something like @ReportAsSingleViolation annotation and it kinda works, but it have fixed message from custom constraint. So it's not what I want.
I've read about @GroupSequence but I can't get it working like I've described either.
This is my entity with custom constraint rules:
@Entity
@Table(name = "users", schema = "myschema")
public class User {
private int id;
@ValidLogin
private String login;
@ValidPassword
private String password;
@ValidEmail
private String email;
//getters & setters
}
And implementation of my custom constraint with couple built-in rules:
@Constraint(validatedBy = UsernameValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@NotEmpty
@Pattern(regexp = "^[a-zA-Z0-9]*$")
@Length.List({
@Length(min = 3 , message = "{Length.min.user.login}"),
@Length(max = 30, message = "{Length.max.user.login}")
})
public @interface ValidLogin {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default { };
}
And by default I get every message for failed rule in my jsp view. So again, I want to get it working like this: check for rule @NotEmpty and if it fails, return appropriate message, if not validate next rule @Pattern and so on.
Could you help? Thanks a lot!
Upvotes: 2
Views: 6752
Reputation: 15204
Each annotation has groups
attribute that could be used for dividing checks to groups:
public class MyForm {
@NotEmpty(groups = LoginLevel1.class)
@Pattern(regexp = "^[a-zA-Z0-9]*$", groups = LoginLevel2.class)
@Length.List({
@Length(min = 3 , message = "{Length.min.user.login}", groups = LoginLevel3.class),
@Length(max = 30, message = "{Length.max.user.login}", groups = LoginLevel3.class)
})
private String login;
}
Next step is to group these groups with @GroupSequence
that allows to have fail-fast behavior:
public class MyForm {
...
@GroupSequence({
LoginLevel1.class,
LoginLevel2.class,
LoginLevel3.class
})
public interface LoginChecks {}
public interface LoginLevel1 {}
public interface LoginLevel2 {}
public interface LoginLevel3 {}
}
The final step is to instruct Spring to validate using these group sequences:
@PostMapping("/form-handler")
public String processForm(@Validated({MyForm.LoginChecks.class, MyForm.PasswordChecks.class}) MyForm form, BindingResult result) {
if (result.hasErrors()) {
return null;
}
...
}
Upvotes: 0
Reputation: 2508
Here's what, I think, you are looking for:
@Test
public void test() {
Validator v = Validation.byProvider( HibernateValidator.class )
.configure()
.buildValidatorFactory()
.getValidator();
// validations for each group - shows only corresponding violations even if other constraints
// are violated as well
assertThat( v.validate( new Bar( null, null ), First.class ) ).hasSize( 2 );
assertThat( v.validate( new Bar( "", "" ), Second.class ) ).hasSize( 2 );
assertThat( v.validate( new Bar( "a", "a" ), Third.class ) ).hasSize( 2 );
// shows that validation will go group by group as defined in the sequence:
//NotNull
Set<ConstraintViolation<Bar>> violations = v.validate( new Bar( null, null ) );
assertThat( violations ).hasSize( 2 );
assertThat( violations ).extracting( "message" ).containsOnly( "must not be null" );
//NotBlank
violations = v.validate( new Bar( "", "" ) );
assertThat( violations ).hasSize( 2 );
assertThat( violations ).extracting( "message" ).containsOnly( "must not be blank" );
//Size
violations = v.validate( new Bar( "a", "a" ) );
assertThat( violations ).hasSize( 2 );
assertThat( violations ).extracting( "message" ).containsOnly( "size must be between 5 and 2147483647" );
}
@GroupSequence({ First.class, Second.class, Third.class, Bar.class })
private static class Bar {
@NotNull(groups = First.class)
@NotBlank(groups = Second.class)
@Size(min = 5, groups = Third.class)
private final String login;
@NotNull(groups = First.class)
@NotBlank(groups = Second.class)
@Size(min = 5, groups = Third.class)
private final String password;
public Bar(String login, String password) {
this.login = login;
this.password = password;
}
}
interface First {
}
interface Second {
}
interface Third {
}
I've added a test so it is visible how validation goes group by group. And to have such behavior you need to redefine a default group sequence for your bean. To do that you need to place @GroupSequence
annotation on your bean that you'd like to validate, then list all the groups you need and don't forget to add the bean class itself (like in this example). Also all of this information is present here - in the documentation.
If you are OK with not using standard constraints you then might do something like:
@Test
public void test2() throws Exception {
Set<ConstraintViolation<Foo>> violations = validator.validate( new Foo( "", null ) );
assertThat( violations ).hasSize( 2 );
assertThat( violations ).extracting( "message" )
.containsOnly( "value should be between 3 and 30 chars long", "Value cannot be null" );
}
private static class Foo {
@ValidLogin
private final String login;
@ValidLogin
private final String password;
public Foo(String login, String password) {
this.login = login;
this.password = password;
}
}
@Target({ FIELD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ValidLogin.ValidLoginValidator.class })
@interface ValidLogin {
String message() default "message";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
class ValidLoginValidator implements ConstraintValidator<ValidLogin, String> {
private static final Pattern PATTERN = Pattern.compile( "^[a-zA-Z0-9]*$" );
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
String message = "";
if ( value == null ) {
message = "Value cannot be null";
}
else if ( !PATTERN.matcher( value ).matches() ) {
message = "Value should match pattern ";
}
else if ( message.length() < 3 || message.length() > 30 ) {
message = "value should be between 3 and 30 chars long";
}
if ( !message.isEmpty() ) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate( message ).addConstraintViolation();
}
return false;
}
}
}
In this case you just have your own custom constraint and validator for it. And you go check by check and then build the violation based on the first failed check. Also you could extract things like pattern
and min
, max
as attributes to your constraint if you have similar checks to perform for login and password but for example with different patterns on string length...
Upvotes: 3
Reputation: 43
I was wondering if GroupSequence didnt't work because of custom constraint and YES, that's why.
I was aplying built-in rules in custom constraint and there, groups for them don't work. In custom constraint was visible only one group DEFAULT, because of:
Class<?>[] groups() default { };
When I moved those built-in rules from custom constraint to field (which is more ugly now, I wanted to keep Entity pretty) this works.
But here we go again. Now it must go "level" by "level" meaning when one field is blank, but the others not, there be only one message for empty one. Others even thogh they are invalid, are waiting for next "level" sequence. Which is again - NOT WHAT I WANT.
Seems like getting one error per field is too much for spring / hibernate.
If anyone hava an idea how to get it working, please, let me know, I'll give it a try.
Upvotes: 0