1
votes

I have a form with ten fields, but a page displays only five fields initially. Remaining fields are hidden, and show up based on onChange event listener bound to other field.

There are scenarios wherein it is possible to submit a form with just five not blank fields. In that case, validation has to be performed only for those five. Sometimes, validation has to be performed for all fields (based on the state of that other field with onChange callback). Is there a way to filter field validation using annotations or should I write a custom validator?

Right now all ten form fields are validated. The rules are the following:

  • a,b,c,d,e are mandatory fields
  • f,g,h shows up only if a="111"
  • i,j shows up only if c="222"

Sample code:

@NotBlank
private String a;
@NotBlank
private String b;
@NotBlank
private String c;
@NotBlank
private String d;
@NotBlank
private String e;

@NotBlank(a="111")
private String f;
@NotBlank(a="111")
private String g;
@NotBlank(a="111")
private String h;
@NotBlank(c="222")
private String i;
@NotBlank(c="222")
private String j;

Controller code:

@RequestMapping(value="/payments/getTax.do", method=RequestMethod.POST) 
public String getTaxInfo(@Valid OneTimeForm command, BindingResult result, 
    Model model, HttpServletRequest request, HttpServletResponse response) throws Exception {        


OneTimeForm oneTimeForm = (OneTimeForm) command;        

try {
    validator.validate(oneTimeForm, result);
} catch (ValidationException ex) {  
    throw ex;
}
...///
4

4 Answers

4
votes

You can validate mandatory fields via Bean Validation annotations, but for more complex conditional logic (i.e. f,g,h show up only if a="111" and i,j show up only if c="222") there is org.springframework.validation.Validator.

The example implementation in your case could be:

public class OneTimeFormValidator implements org.springframework.validation.Validator {

    @Override
    public boolean supports(Class clazz) {
       return OneTimeForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        final OneTimeForm form = (OneTimeForm) target;
        if ("111".equals(form.getA())) {
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "f", "f.required");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "g", "g.required");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "h", "h.required");
        }
        if ("222".equals(form.getC())) {
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "i", "i.required");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "j", "j.required");
        }
    }
 }

ValidationUtils.rejectIfEmptyOrWhitespace(Errors, String, String) is the same to @NotBlank behaviorwise. Therefore do not annotate optional fields:

@NotBlank
private String a;
@NotBlank
private String b;
@NotBlank
private String c;
@NotBlank
private String d;
@NotBlank
private String e;

private String f;
private String g;
private String h;
private String i;
private String j;

Your controller could look like:

...

@Autowired
private org.springframework.validation.Validator oneTimeFormValidator;

@RequestMapping(value="/payments/getTax.do", method=RequestMethod.POST)  
public String getTaxInfo(@Valid OneTimeForm command, BindingResult result) { 

     // validates conditional logic with OneTimeFormValidator
     oneTimeFormValidator.validate(command, result);

     if (result.hasErrors()) {
         // return a page
     }
     // do something else and return a page
}

...

Mandatory fields are validated with provided instance of type javax.validation.Validator through @Valid. Encountered validation errors are put into BindingResult instance. Then the conditional validation is performed via OneTimeFormValidator, and encountered validation errors are put into the same instance of BindingResult.

2
votes

You should be able to run validator manually and filter off errors which you don't want to return to client. To be able to execute validator manually I had to add BindingResult as argument of rest method implementation. Without BindingResult Spring framework didn't invoke method but returned validation errors to client.

@RequestMapping(method = PATCH, value = "/{id}")
public ResponseEntity<User> patchUser(@RequestBody User patchUser,
                                      BindingResult bindingResult)  {

I could provide more accurate answer if you describe your controller.

Thanks for more details. In my solution I removed @Valid annotation from controller definition so I have empty bindingResult. Then I validate programmatically and pick out validation errors which I want to return to client.

BindingResult allErrors = new BeanPropertyBindingResult(target, objectName);
//validate and copy relevant errors into bindingResult
validator.validate(target, allErrors);
for (FieldError fe : allErrors.getFieldErrors()) {
        if (/* your conditions */) {
            bindingResult.addError(fe);
        }
    }
0
votes

I can suggest two ways to do that.

You could create little models and validate against that. On validation successful, you build your entity.

Or you could create your own validation constraint and validate the whole class onSubmit.