Monday, 25 July 2011

Applying a Custom Spring Validator to a Spring MVC Controller

Last month I wrote a blog demonstrating how to write a Spring validator using Spring’s Validator interface. Today’s blog demonstrates how to apply that validator to the Controller of a Spring 3 web-application.

Before I do that, I have to say that this technique should really be marked as deprecated in favour of using JSR 303 built-in and custom constraints and that it should be avoided if at all possible.

You may remember, if you’ve read my previous blog, that the contrived scenario for this example was of a postal code validator that checked for a Birmingham post code: the ad-hoc rule being that the first character of the post code string had to be the letter ‘B’.


The MVC command object is a simple matter of tying together a few address fields:

public class Address {

 
private String street;

 
private String town;

 
private String country;

 
private String postCode;

 
public String getStreet() {

   
return street;
 
}

 
public void setStreet(String street) {

   
this.street = street;
 
}

 
public String getTown() {

   
return town;
 
}

 
public void setTown(String town) {

   
this.town = town;
 
}

 
public String getCountry() {

   
return country;
 
}

 
public void setCountry(String country) {

   
this.country = country;
 
}

 
public String getPostCode() {

   
return postCode;
 
}

 
public void setPostCode(String post_code) {

   
this.postCode = post_code;
 
}
}

Since my last blog I’ve simplified my validator so that it only does the one test as outlined above.

@Component
public class AddressValidator implements Validator {

 
/**
   * Return true if this object can validate objects of the given class. This is cargo-cult
   * code: all implementations are the same and can be cut 'n' pasted from earlier examples.
   */
 
@Override
 
public boolean supports(Class<?> clazz) {

   
return clazz.isAssignableFrom(Address.class);
 
}

 
/**
   * Validate an object, which must be a class type for which the supports() method returned
   * true.
   *
   *
@param obj The target object to validate
   *
@param errors contextual state info about the validation process (never null)
   */
 
@Override
 
public void validate(Object obj, Errors errors) {

   
Address address = (Address) obj;
    String postCode = address.getPostCode
();
    validatePostCode
(postCode, errors);
 
}

 
private void validatePostCode(String postCode, Errors errors) {

   
if (isValidString(postCode) && isNotBirminghamPostCode(postCode)) {
     
errors.rejectValue("postCode", "AddressValidator.postCode.notBirmingham",
         
"Not a Birmingham Post Code");
   
}
  }

 
private boolean isValidString(String str) {

   
return isNotNull(str) && (str.length() > 0);
 
}

 
private boolean isNotNull(String postCode) {

   
return postCode != null;
 
}

 
/** The first character of the Birmingham post code is 'B' */
 
private boolean isNotBirminghamPostCode(String postCode) {

   
char val = postCode.charAt(0);
   
return val != 'B';
 
}
}

Attaching the validator to the controller is pretty straight forward. The first step is to annotate the Address command object with @Valid:

  @RequestMapping(value = PATH, method = RequestMethod.POST)
 
public String addAddress(@Valid Address address, BindingResult result, Model model) {

The second step is to inject the validator into the data binder:

  @InitBinder
 
protected void initBinder(WebDataBinder binder) {

   
binder.setValidator(addressValidator);
 
}

Adding these two code snippets together, the complete AddressController code looks like this:

@Controller
public class AddressController {

 
private static final String FORM_VIEW = "address.page";

 
private static final String PATH = "/address";

 
@Autowired
 
private AddressValidator addressValidator;

 
/**
   * Create the initial blank form
   */
 
@RequestMapping(value = PATH, method = RequestMethod.GET)
 
public String getCreateForm(Model model) {

   
model.addAttribute(new Address());
   
return FORM_VIEW;
 
}

 
/**
   * Attach the custom validator to the Spring context
   */
 
@InitBinder
 
protected void initBinder(WebDataBinder binder) {

   
binder.setValidator(addressValidator);
 
}

 
/**
   * This is the handler method. Check for errors and proceed to the next view
   */
 
@RequestMapping(value = PATH, method = RequestMethod.POST)
 
public String addAddress(@Valid Address address, BindingResult result, Model model) {

   
if (!result.hasErrors()) {
     
model.addAttribute("noErrors",
         
"No Errors This Time for postal code: " + address.getPostCode());
   
}

   
return FORM_VIEW;
 
}
}

This technique should be used when you need to do ALL your controller’s validation yourself, and you can’t or don’t want to make use of the Hibernate’s reference implementation of a JSR 303 validator. From this, you’ll guess that you can’t mix your own custom Spring validator with Hibernate’s JSR 303 validator. For example, adding the built-in annotations to the Address command object will have no effect:

public class Address {

 
// These JSR 303 built in annotations don't do anything
  // When you've injected your own validator
 
@NotEmpty
  @Size
(min = 1, max = 12)
 
private String street;

 
@NotEmpty
 
private String town;

 
@NotEmpty
 
private String country;

 
@NotEmpty
 
private String postCode;

If you want to mix together Hibernate’s built-in constraints with your own custom validation, then you’ll need to write your own JSR303 custom constraint, but more on that another day....

5 comments:

Anonymous said...

Great blog!! I am wondering if it is possible to create a custom validator like this one for several classes? Or do I need to create one for each class? Thank you

Roger Hughes said...

Hi Anonymous
Yes you can use this validator to validate several classes. One way of achieving this would be to use a bit of reflection in the

public void validate(Object obj, Errors errors)

method to determine the class type. You can then cast the Object accordingly and do the validation.

Have you looked at JSR 303? It may not be suitable for your business case, but if it is then it'll make life easier.

Anonymous said...

Great Info.
However, My Scenario Is Slightly Different.
I Need I Validate An Info Object.
IndiA
Hierarchy Of Info
>InfoA
>>InfoA1
>>InfoA2
>InfoB
>>InfoBA
>>InfoBB

That' Assume That Info Have 20 Attribues, But That's Only Consider AttOne And AttTwo, AttThree
AttOne, AttThree Is Mandatory To InfoA But Optional To InfoB. AttTwo Is mandatory For InfoA1 And InfoB.

Is It Possible To Make Use Of TheJSR303?
Sorry For The Caps.
Thanks.

Roger Hughes said...

Thanks for the comment. I should think that you could do what you want using JSR303. You can find more on JSR303 starting here or simply entering JSR303 in the top right corner of the screen.

Anonymous said...

Hi Roger,Thanks For The Prompt Reply.I Will Dig More Info.

P.S: Sorry Again For Caps My Phone Just Decide To Caps Every Word.