spring-mvchibernate-validatorspring-4spring-mvc-initbinders

Conflicts: Spring MVC with Hibernate Validator - Submit/Binding action


I have two entities, they are: Person and Address, relation one to one. The source code about JPA/Hibernate has been removed to keep the things simple.

My problem is with Hibernate Validator.

I have the following for the Person class:

@Valid
@NotNull(message="{person.address.null}", groups={AddressCheck.class})
public Address getAddress() {
    return address;
}

and for Address:

@NotNull(message="{field.null}", groups=AddressCheck.class)
public Person getPerson() {
    return person;
}

Note: About JUnit, all works fine, if some dependency is null the respective error is created.

The problem is when Hibernate Validator is integrated with Spring MVC, it through the binding approach

About the view code, below the JSP fragment for the Address class

<form:form modelAttribute="person"  method="post" >
...
<fieldset>
    <legend><spring:message code="address.form.legend"/></legend>
    <table>
        <form:hidden path="address.id"/> 
        <tr>
            <td><spring:message code="address.form.street"/></td>
            <td><form:input path="address.street" size="40"/></td>
            <td><form:errors path="address.street" cssClass="error"/></td>
        </tr>
        <tr>
            <td><spring:message code="address.form.number"/></td>
            <td><form:input path="address.number" size="40"/></td>
            <td><form:errors path="address.number" cssClass="error"/></td>
        </tr>
    </table>
</fieldset>

To create the form view, to let the JSP render both objects, I have the following:

@RequestMapping(value="/register.htm", method=RequestMethod.GET)
public String createRegisterForm(Model model){
    logger.info("createRegisterForm GET");
    Person person = new Person();
    Address address = new Address();
    address.setId("5");//just to play/test

    person.setAddress(address);
    address.setPerson(person);

    model.addAttribute("person", person);
    return "jsp/person/registerForm";
}

Observe how the two setters are called.

When I do the submit the following code must be executed

@RequestMapping(value="/register.htm", method=RequestMethod.POST)
public String registerPerson( @Validated(PersonRegistrationOrdered.class) Person person, 
                              BindingResult result,
                              Model model){
    logger.info("registerPerson POST");
    logger.info("{}", person.toString());
    logger.info("{}", person.getAddress().toString());

    if(result.hasErrors()){
        logger.error("There are errors!!!!");
        model.addAttribute("person", person);

        for(ObjectError objectError : result.getAllErrors()){
            logger.error("Error {}", objectError);
        }

        return "jsp/person/registerForm";
    }
    logger.info("All fine!!!!");
    return "jsp/manolo";
}

Sadly the console shows an error about the Person object from the Address class is null, even when in the createRegisterForm it has been related address.setPerson(person);

My unique way to around this is disabling the validation

//@NotNull(message="{field.null}", groups=AddressCheck.class)
public Person getPerson() {
    return person;
}

First Question: how I can avoid this without disabling the @NotNull validation?

Even more, if I want protect the id for the Address class:

@InitBinder
public void initBinder(WebDataBinder binder) {
    ...
    binder.setDisallowedFields("address.id");
}

The HTML code generated from the JSP file is

<fieldset>
    <legend>Address:</legend>
    <table>
        <input id="address.id" name="address.id" type="hidden" value="5"/> 
        <tr>
            <td>Street:</td>
            <td><input id="address.street" name="address.street" type="text" value="" size="40"/></td>
            <td></td>
        </tr>
        <tr>
            <td>Number:</td>
            <td><input id="address.number" name="address.number" type="text" value="" size="40"/></td>
            <td></td>
        </tr>
    </table>
</fieldset>

But again when I do the submit I get

- ValidationMessages found.
- org.hibernate.validator.ValidationMessages found.
- registerPerson POST
- Person [id=5, firstName=Manuel, lastName=Jordan, alive=true, dateBirth=Wed Dec 12 00:00:00 PET 2012, dateRetirement=null, dateDeath=null, age=15, weight=34.0, salary=1500]
- Address [id=null, street=La blanquita, number=155]
- There are errors!!!!
- Error Field error in object 'person' on field 'address.id': rejected value [null]; codes [NotNull.person.address.id,NotNull.address.id,NotNull.id,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.address.id,address.id]; arguments []; default message [address.id]]; default message [The field must be not empty]

If I am creating a new Person and using perhaps a special class giving a potential id (5 for example address.setId("5"); in this case) for the Address's id it is going to be a problem.

Second Question: how I can turn around this problem?


Solution

  • The problem is that you are creating your model in the method with GET. Which means as soon as the page is rendered the model object is gone. When the POST comes in a new empty Person is created and due to automatic path expansion also an Address but this will not have the relationship correctly applied.

    There are 2 ways to fix this.

    1. Create a method annotated with @ModelAttribute which constructs the model needed to use for binding.
    2. Added @SessionAttributes to store the Person in the session in between requests.

    Another thing to make either option work is next to @Validated you will need to add @ModelAttribute to your method Person method argument.

    Method with @ModelAttribute

    First add the method which creates the model object. For this create a method and annotate it with @ModelAttribute and optionally give it a name of the model object person. (Which would also be the default, as that is to use the classname with a lowercase first letter).

    @ModelAttribute("person")
    public Person prepareModel() {
        Person person = new Person();
        Address address = new Address();
        address.setId("5");//just to play/test
    
        person.setAddress(address);
        address.setPerson(person);
        return person;
    }
    

    Next modify your GET and POST methods. First off all for both methods you can remove the Model as a method argument as that isn't needed anymore. The Person is already added to the model.

    @RequestMapping(value="/register.htm", method=RequestMethod.GET)
    public String createRegisterForm(){
        logger.info("createRegisterForm GET");
        return "jsp/person/registerForm";
    }
    

    And for the POST add @ModelAttribute and you don't need to

    @RequestMapping(value="/register.htm", method=RequestMethod.POST)
    public String registerPerson( @Validated(PersonRegistrationOrdered.class) @ModelAttribute Person person, 
                                  BindingResult result){
        logger.info("registerPerson POST");
        logger.info("{}", person.toString());
        logger.info("{}", person.getAddress().toString());
    
        if(result.hasErrors()){
            logger.error("There are errors!!!!");
    
            for(ObjectError objectError : result.getAllErrors()){
                logger.error("Error {}", objectError);
            }
    
            return "jsp/person/registerForm";
        }
        logger.info("All fine!!!!");
        return "jsp/manolo";
    }
    

    Using @SessionAttributes

    Using @SessionAttributes you can identify which objects to store, temporary, in the HttpSession. Add this annotation to your controller classes and in your POST method add SessionStatus as an argument and remove the Model as that will already be populated.

    @Controller
    @SessionAttributes(Person.class)
    public class RegisterController {
    
        @RequestMapping(value="/register.htm", method=RequestMethod.POST)
        public String registerPerson( @Validated(PersonRegistrationOrdered.class) @ModelAttribute Person person, 
                                      BindingResult result, SessionStatus status){
            logger.info("registerPerson POST");
            logger.info("{}", person.toString());
            logger.info("{}", person.getAddress().toString());
    
            if(result.hasErrors()){
                logger.error("There are errors!!!!");
    
                for(ObjectError objectError : result.getAllErrors()){
                    logger.error("Error {}", objectError);
                }
    
                return "jsp/person/registerForm";
            }
            logger.info("All fine!!!!");
            status.setComplete();
            return "jsp/manolo";
        }
    
    }
    

    You need to call the setComplete() method on the SessionStatus so that the session gets cleaned of obsolete model objects.