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.
Edit
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...