Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to apply JSF Validator after annotations constraints?

My problem is more about performance than functionality. I am dealing with an email field for a sign up form. For its validation, I use annotations in the User entity for the email format, and an EmailExistValidator to check in the database if this email is already used.

@Entity
@Table(name = "Users")
public class User {

    @Column(name = "email")
    @NotNull(message = "Email address required")
    @Pattern(regexp = "([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)", message = "Invalid email address")
    private String email;

    // ... (other fields and getters/setters here)
}

The validator:

@FacesValidator(value = "emailExistValidator")
public class EmailExistValidator implements Validator {

    private static final String EMAIL_ALREADY_EXISTS = "This email address is already used.";

    @EJB
    private UserDao userDao;

    @Override
    public void validate(FacesContext context, UIComponent component, Object value)
            throws ValidatorException {
        String email = (String) value;
        try {
            if (email != null && userDao.findByEmail(email) != null) {
                throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR,
                        EMAIL_ALREADY_EXISTS, null));
            }
        } catch (DaoException e) {
            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getMessage(),
                    null);
            FacesContext facesContext = FacesContext.getCurrentInstance();
            facesContext.addMessage(component.getClientId(facesContext), message);
        }
    }
}

According to my tests, if an email does not verify the regexp pattern, the validator is still applied. I don't like the idea of performing a database query to check for email existence while I already know this email is not even valid. It could really slow down the performance (even more when using ajax on blur events in the form).

I have some guesses but I couldn't find clear answers, that's why I ask a few questions here:

  1. Is this a bad thing to mix a validator with annotation constraints?

  2. During the validation, is every constraint checked even if one of them does not pass? (If yes, then are all the messages (for each constraint) added to the FacesContext for the component, or just the first message?)

  3. If yes to question 2, is there any way to force the validation to stop as soon as one constraint is not verified?

  4. If no to question 2 or yes to question 3, is the order of application of the constraints/validator specified somewhere? Is this order customizable?

If this matters, I'm using PrimeFaces for my Facelets components.

I know I could put everything in the validator and stop whenever, but this validator is to be used only for the Sign Up form. When used for signing in, I would only check the entity's annotation constraints, not the "already exists" part. In addition, annotations and single constraint validators are to my mind more readable than the content of a multi-constraint validator.

like image 666
Joffrey Avatar asked Nov 20 '25 14:11

Joffrey


1 Answers

JSF validation runs by design before JSR303 bean validation. So there's technically no way to skip JSF validation when bean validation fails. If they would run the other way round, then you would theoretically simply have checked UIInput#isValid() in the JSF validator:

UIInput input = (UIInput) component;

if (!input.isValid()) {
    return;
}

There's unfortunately no API-provided way to control the JSF-JSR303 validation order. Your best bet is to turn the JSF validator into a true JSR303 bean validator and assign it to a different group which you declare to run after the default group. In JSR303, constraints are validated on a per-group basis. If one validation group fails, then any subsequent validation groups are not executed.

First create a custom JSR303 bean validation constraint annotation:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
@Documented
public @interface UniqueEmail {
    String message() default "{invalid.unique.email}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Then create a custom JSR303 bean validation constraint validator:

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    @Override
    public void initialize(UniqueEmail annotation) {
        // Grab EJB here via JNDI?
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return userDao.findByEmail(value) == null;
    }

}

Unfortunately, @EJB/@Inject in a ConstraintValidator isn't natively supported in Java EE 6 / JSR303 Bean Validation 1.0. It's only supported in Java EE 7 / JSR303 Bean Validation 1.1. They are however available via JNDI.

Then create a custom validation group:

public interface ExpensiveChecks {}

And finally use it on your entity whereby the group ordering is declared via @GroupSequence annotation (the default group is identified by current class):

@Entity
@Table(name = "Users")
@GroupSequence({ User.class, ExpensiveChecks.class })
public class User {

    @Column(name = "email")
    @NotNull(message = "Email address required")
    @Pattern(regexp = "([^.@]+)(\\.[^.@]+)*@([^.@]+\\.)+([^.@]+)", message = "Invalid email address")
    @UniqueEmail(groups = ExpensiveChecks.class, message = "This email address is already used")
    private String email;

    // ...
}
like image 65
BalusC Avatar answered Nov 22 '25 16:11

BalusC