2

I have the following code that allows a user to update name and year.

Model

@Entity
public class Person implements Serializable{

    private static final long serialVersionUID = 1L;

    private String name;

    private int year;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

}

Controller

@RequestMapping(value = "/person", method = RequestMethod.POST)
public ModelAndView editPerson(@ModelAttribute Person person)
{
    //perform operations

    //display results
    ModelAndView modelAndView = new ModelAndView("Person.html");
    modelAndView.addObject("personBind", person);
        
    return modelAndView;

}

View

<form action="person" method="post" th:object="${personBind}">
Name:
<input type="text" th:field="*{name}" />

Year:   
<input type="text" th:field="*{year}" />

Now I want to do some validation in the year field. For instance, if the user inputs a string instead of a number on that field, the current code will throw an exception because it doesn't allow to set a string in an integer attribute.

So how to validate the input? Don't want to use @Valid. Want to do some custom validations.

The way I found to do this is to create a string version of the year field in the model (getter/setter). Then use that strYear in the view and do the validation in the controller. Like the updated code below. Is that the correct approach or there's a better way to do this? I ask because not sure if it's correct to create a string version of getters/setters for every numeric attribute that needs to be validated. Seems a lot of duplication.

Model

@Entity
public class Person implements Serializable{

    private static final long serialVersionUID = 1L;

    private String name;

    private int year;

    @Transient
    private String strYear;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }    
    
    public String getStrYear() {
        return strYear;
    }

    public void setStrYear(String strYear) {
        this.strYear = strYear;
    }

}

Controller

@RequestMapping(value = "/person", method = RequestMethod.POST)
public ModelAndView editPerson(@ModelAttribute Person person)
{
    //validate
    boolean valid = Validate(Person.getStrYear());

    if(valid==true)
    {
      Person.setYear(Integer.ParseInt(Person.getStrYear()));
      //save edit
    }
    else
    {//display validation messages}

    //display results
    ModelAndView modelAndView = new ModelAndView("Person.html");
    modelAndView.addObject("personBind", person);
        
    return modelAndView;

}

View

<form action="person" method="post" th:object="${personBind}">
Name:
<input type="text" th:field="*{name}" />

Year:   
<input type="text" th:field="*{strYear}" />
jkfe
  • 549
  • 7
  • 29
  • What is the thought of not wanting to use the @Valid annotation? Because, that is generally the way that we do validation in Spring MVC. – hooknc Dec 07 '20 at 17:33
  • Couple reasons. 1) I have some fields in my form that are not in the model entity. Understand that cannot be validated by `@Valid`. So thought would do all of the validations the same way, not part with `@Valid` and part in a different way. 2) Not sure how I can perform custom validations with `@Valid` (let's say I need to perform a calculation to say the input is correct or even check if there's a string being inputed in a number field like in this question I posted). Maybe there's how, it's just that I don't know. – jkfe Dec 07 '20 at 17:49
  • Validation can be tricky, but all the problems that you have brought up can be addressed via the @Valid annotation. I'll get an answer together for you, but it will take a bit of time to get it written out correctly. – hooknc Dec 07 '20 at 17:55
  • 2
    https://www.baeldung.com/spring-mvc-custom-validator try custom validator – gladiator Dec 07 '20 at 18:13
  • @hooknc Another issue I'm facing when using `@Valid` is the one I posted on this question: https://stackoverflow.com/questions/65190605/how-to-do-validation-in-spring-mvc-when-theres-a-dto – jkfe Dec 07 '20 at 23:15
  • "I have some fields in my form that are not in the model entity" -> That is why I almost never use my entity for the controller. I would create a dedicated object (e.g. `PersonFormData`) for this. – Wim Deblauwe Dec 08 '20 at 10:26
  • @WimDeblauwe Do you have an example to point out on how to do this additional layer (PersonFormData)? I'm thinking that's going to be the best alternative as I'm facing some issues working with the entity on the controller. – jkfe Dec 08 '20 at 16:57

1 Answers1

1

Validation can be tricky and difficult, some things to take into account when doing validation...

Validation Considerations

  • The model in MVC (Model, View, Controller) does not, and generally should not, be the same as your Domain Model. See @wim-deblauwe's comment and the Q&A section below.

    • Often times what is displayed in the user interface is different than what is available inside the Domain Model.
    • Placing @Valid annotations into your Domain Model means that in every form that Domain Model is used, the same @Valid rules will apply. This is not always true. Side Note: This might not apply to super simple CRUD (Create, Read, Update, Delete) applications, but in general, most applications are more sophisticated that just pure CRUD.
  • There are SERIOUS security issues with using a real Domain Model object as the form backing object due to the way that Spring does auto setting of values during form submittal. For example, if we are using a User object that has a password field on it as our form backing object, the form could be manipulated by browser developer tools to send a new value for the password field and now that new value will get persisted.

  • All data entered via an html form is really String data that will need to get transposed to its real data type (Integer, Double, Enumeration, etc...) later.

  • There are different types of validation that, in my opinion, need to happen in different temporal order.

    • Required checks happen before type checking (Integer, Double, Enumeration, etc...), valid value ranges, and then finally persistence checks (uniqueness, previous persisted values, etc...)
    • If there are any errors in a temporal level, then don't check anything later.
      • This stops the end user from getting errors like, phone number is required, phone number isn't a number, phone number isn't formatted correctly, etc... in the same error message.
  • There shouldn't be any temporal coupling between validators. Meaning that if a field is optional, then 'data type' validator shouldn't fail validation if a value isn't present. See the validators below.

Example

Domain Object / Business Object:

@Entity
public class Person {

    private String identifier;
    private String name;
    private int year;

    public String getIdentifier() {
        return identifier;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }    
}

To populate an html form via a Spring MVC Controller we would create a specific object that represents that form. This also includes all the validation rules as well:

@GroupSequence({Required.class, Type.class, Data.class, Persistence.class, CreateOrUpdatePersonForm.class})
public class CreateOrUpdatePersonForm {

    @NotBlank(groups = Required.class, message = "Name is required.")
    private String name;

    @NotBlank(groups = Required.class, message = "Year is required.")
    @ValidInteger(groups = Type.class, message = "Year must be a number.")
    @ValidDate(groups = Data.class, message = "Year must be formatted yyyy.")
    private String year;

    public CreateOrUpdatePersonForm(Person person) {
        this.name = person.getName();
        this.year = Integer.valueOf(person.getYear);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getYearStr() {
        this.year;
    }

    public void setYearStr(String year) {
        this.year = year;
    }

    public int getYear() {
        return Integer.valueOf(this.year);
    }
}

Then in your controller to use the new CreateOrUpdatePersonForm object:

@Controller
public class PersonController {
    ...
    @ModelAttribute("command")
    public CreateOrUpdatePersonForm setupCommand(@RequestParam("identifier") Person person) {

        return new CreateOrUpdatePersonForm(person);
    }

    //@PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.GET)
    public ModelAndView getForm(@RequestParam("person") Person person) {

        return new ModelAndView("/form/person");
    }

    //@PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.POST)
    public ModelAndView postForm(@RequestParam("person") Person person, @ModelAttribute("command") @Valid CreateOrUpdatePersonForm form,
                                 BindingResult bindingResult, RedirectAttributes redirectAttributes) {

        ModelAndView modelAndView;

        if (bindingResult.hasErrors()) {

            modelAndView = new ModelAndView("/form/person");

        } else {

            this.personService.updatePerson(person.getIdentifier(), form);

            redirectAttributes.addFlashAttribute("successMessage", "Person updated.");

            modelAndView = new ModelAndView("redirect:/person/" + person.getIdentifier() + ".html");
        }

        return modelAndView;
    }
}

The @ValidInteger and @ValidDate are validators that we wrote ourselves.

@ValidInteger:

public class ValidIntegerValidator implements ConstraintValidator<ValidInteger, String> {

    @Override
    public void initialize(ValidInteger annotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

        boolean valid = true;

        if (StringUtils.hasText(value)) {

            try {
                Integer.parseInteger
(value);

            } catch (NumberFormatException e) {

                valid = false;
            }
        }

        return valid;
    }
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidIntegerValidator.class)
@Documented
public @interface ValidInteger {

    String message() default "{package.valid.integer}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

@ValidDate

public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {

    private String format;

    @Override
    public void initialize(ValidDate annotation) {
        this.format = annotation.format();
    }

    @Override
    public boolean isValid(String inputDate, ConstraintValidatorContext constraintValidatorContext) {

        boolean valid = true;

        if (StringUtils.hasText(inputDate)) {

            SimpleDateFormat dateFormat = new SimpleDateFormat(format);

            dateFormat.setLenient(false);

            try {

                dateFormat.parse(inputDate);

            } catch (ParseException e) {

                valid = false;
            }
        }

        return valid;
    }
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidDateValidator.class)
@Documented
public @interface ValidDate {

    String message() default "{package.dateformat}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String format();
}

Then in your view jsp or template you'll need to display the errors if there are any:

<html>
    ...
    <body>
        <common:form-errors modelAttribute="command"/>
        ...
    </body>
</html>

There is a lot more to deal with validation, like comparing two fields together, or accessing your persistence layer to verify that a person name is unique, but that takes a lot more explanation.

Q & A

Q: Can you provide links that explain the thought behind not using the Domain Model as the MVC Model?

A: Sure, Entities VS Domain Models VS View Models and Entity vs Model vs View Model

TL;DR: Using different objects for the Domain Model and the MVC Model is because it reduces coupling between application layers and it protects our UI and Domain Model from changes in either layer.

Other Considerations

Validation of data needs to occur at all entry points to an application: UI, API, and any external systems or files that are read in.

An API is just a UI for computers and needs to follow the same rules as the human UI.

Accepting data from the internet is fraught with peril. It is better to be more restrictive than less restrictive. This also includes making sure that there aren't any strange characters coughMicrosoft's 1252 character encodingcough, Sql Injection, JavaScript Injection, making sure that your database is setup for unicode and understanding that a column that is setup for 512 characters, depending on the language can only actually handle 256 characters due to codepoints.

hooknc
  • 4,854
  • 5
  • 31
  • 60
  • 1
    Awesome very detailed answer. This is very helpful. Based on this I see I have some changes I need to do. Thanks a lot for your time and effort to put this together. Very much appreciated. – jkfe Dec 08 '20 at 22:16
  • Thanks, I am glad that it was useful. – hooknc Dec 08 '20 at 22:18
  • Do you have some links to point out with more information about this design of having a class representing the form? Would like to learn more about it. All the examples I see bind the model directly to the form without this form class in the middle. But from what I'm experiencing it makes all the sense to have it. – jkfe Dec 08 '20 at 22:42
  • I'll look, there are not a lot of examples showing this because it is 'easier' to just use the Domain Model classes and I feel that is why most examples do that. In fact, in the project that I'm working on we use our Domain Objects for non-form based views. But for our forms we always use class that specifically represents the form. – hooknc Dec 08 '20 at 23:15
  • @jkfe Since you explicitly ask for resources: I explain this in my book [Taming Thymeleaf](https://leanpub.com/taming-thymeleaf) in chapter 11 on forms. You can see the sources of the example at https://github.com/wimdeblauwe/taming-thymeleaf-sources for free. – Wim Deblauwe Dec 09 '20 at 07:55
  • @Wim Deblauwe Awesome. Thank you. – jkfe Dec 09 '20 at 12:32
  • Added a Q&A section, and other considerations section. – hooknc Dec 09 '20 at 20:16