How to perform JSF validation in actionListener or action method?
Asked Answered
R

5

17

I have Bean validation working nicely in my application. Now I want to check that a new user does not choose a username that has already been chosen.

In the actionlistener I have the code that checks the database but how do I force the user to be sent back to the page they were on if they choose an already existing username?

Rockwood answered 5/5, 2011 at 3:12 Comment(1)
I think you shouldn't redirect to another page in the first place, unless the username is valid.Simoniac
A
19

Introduction

You can do it, but JSF ajax/action/listener methods are semantically the wrong place to do validation. You actually don't want to get that far in JSF lifecycle if you've wrong input values in the form. You want the JSF lifecycle to stop after JSF validations phase.

You want to use a JSR303 Bean Validation annotation (@NotNull and friends) and/or constraint validator, or use a JSF Validator (required="true", <f:validateXxx>, etc) for that instead. It will be properly invoked during JSF validations phase. This way, when validation fails, the model values aren't updated and the business action isn't invoked and you stay in the same page/view.

As there isn't a standard Bean Validation annotation or JSF Validator for the purpose of checking if a given input value is unique according the database, you'd need to homegrow a custom validator for that.

I'll for both ways show how to create a custom validator which checks the uniqueness of the username.

Custom JSR303 Bean Validation Annotation

First create a custom @Username constraint annotation:

@Constraint(validatedBy = UsernameValidator.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
public @interface Username {
    String message() default "Username already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

With this constraint validator (note: @EJB or @Inject inside a ConstraintValidator works only since CDI 1.1; so if you're still on CDI 1.0 then you'd need to manually grab it from JNDI):

public class UsernameValidator implements ConstraintValidator<Username, String> {

    @EJB
    private UserService service;

    @Override
    public void initialize(Username constraintAnnotation) {
        // If not on CDI 1.1 yet, then you need to manually grab EJB from JNDI here.
    }

    Override
    public boolean isValid(String username, ConstraintValidatorContext context) {
        return !service.exist(username);
    }

}

Finally use it as follows in model:

@Username
private String username;

Custom JSF Validator

An alternative is to use a custom JSF validator. Just implement the JSF Validator interface:

@ManagedBean
@RequestScoped
public class UsernameValidator implements Validator {

    @EJB
    private UserService userService;

    @Override
    public void validate(FacesContext context, UIComponent component, Object submittedAndConvertedValue) throws ValidatorException {
        String username = (String) submittedAndConvertedValue;

        if (username == null || username.isEmpty()) {
            return; // Let required="true" or @NotNull handle it.
        }

        if (userService.exist(username)) {
            throw new ValidatorException(new FacesMessage("Username already in use, choose another"));
        }
    }

}

Finally use it as follows in view:

<h:inputText id="username" ... validator="#{usernameValidator}" />
<h:message for="username" />

Note that you'd normally use a @FacesValidator annotation on the Validator class, but until the upcoming JSF 2.3, it doesn't support @EJB or @Inject. See also How to inject in @FacesValidator with @EJB, @PersistenceContext, @Inject, @Autowired.

Andras answered 5/5, 2011 at 11:50 Comment(3)
Thanks BalusC. Will this validator be called before or after the bean validation does its checks or the order is not specified?Rockwood
JSF validation is executed before Bean validation. But if the value is null or empty then JSF validation other than required won't be executed. So if you've a @NotNull without required="true", then it will be executed first. But if it is not empty and you have for example a @Pattern, then the JSF validator will be called first.Andras
You are awesome. but can i not just use the normal jsf validator but only validate the property using bean validation? If it is valid then I can check the database. I tried the validateProperty method of the Validator class but I couldn't just get it to work because it was asking for groups and other semantics I didn't understandRockwood
M
4

Yes you can. You can do validation in action listener method, add faces messages if your custom validation failed, then call FacesContext.validationFailed() just before return.

The only problem with this solution is, it happens after the JSF validation and bean validation. I.e., it is after the validation phase. If you have multiple action listeners, say listener1 and listener2: if your custom validation in listener1 failed, it will continue to execute listener2. But after all, you'll get validationFailed in AJAX response.

Medley answered 9/2, 2012 at 14:35 Comment(1)
If this is done in an actionListener as commented, then you will need to make sure that the action method checks facesContext.isValidationFailed() continuing.Fraxinella
A
1

It's better to use action method instead of actionListener for this purpose. Then you can return null (reloads page that triggered the action) from this method if the username exists. Here's an example:

in the facelet:

<h:commandButton action="#{testBean.doAction}" value="and... Action"/>

in the bean:

public String doAction() {
   if (userExists) {
     return null;
   } else {
     // go on processing ...
   }
}
Altitude answered 5/5, 2011 at 6:36 Comment(1)
Providing correct feedback to the end-user is the problem here.Geter
W
0

If you want to provide feedback to end-user:

xhtml:

    <p:commandButton value="Go" process="@this" action="#{myBean.checkEntity()}" oncomplete="if(args.validationFailed){PF('widgetOldInfoNotice').show();}"/>

    <p:confirmDialog id="dialogOldInfoNotice" header="NOTICE" severity="alert" widgetVar="widgetOldInfoNotice">
    -- feedback message--
<p:button value="Ok" onclick="PF('widgetOldInfoNotice').hide();"/>
    </p:confirmDialog>

bean:

public String checkEntity() {
    if (!dao.whateverActionToValidateEntity(selectedEntity)) {
        FacesContext context = FacesContext.getCurrentInstance();
        context.validationFailed();
        return "";
    }
    return "myPage.xhtml";
}
Warlike answered 20/6, 2019 at 23:52 Comment(0)
A
-1

You can define a navigation case in the faces-config.xml file. This will allow you to redirect the user to a given page depending on the return value of the bean.

In the example below a suer is redirected to one of two pages depending on the return value of "myMethod()".

 <navigation-rule>
  <from-view-id>/index.xhtml</from-view-id>
  <navigation-case>
   <from-action>#{myBean.myMethod()}</from-action>
   <from-outcome>true</from-outcome>
   <to-view-id>/correct.xhtml</to-view-id>
  </navigation-case>
  <navigation-case>
   <from-action>#{myBean.myMethod()}</from-action>
   <from-outcome>false</from-outcome>
   <to-view-id>/error.xhtml</to-view-id>
  </navigation-case>
 </navigation-rule>
Adamik answered 5/5, 2011 at 8:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.