Simple Spring Boot CRUD application with Server-side form validation

Published by Alexander Braun on 15 Dec 2017 - tagged with Spring, Spring Boot, Bootstrap, Java, Thymeleaf

This post describes how to add server-side form validation to an existing Spring Boot web application. The example code uses Thymeleaf Template Engine, Bootstrap 3 and Spring JPA in conjunction with HSQL DB.

Introduction

The example code is based on the previous post that described how to set up simple Spring Boot CRUD application.

This time we add server-side form validation to the existing web application. As Spring has built in support for the Java validation API, we are going to use this framework.

Validation Rules

The existing application already allows you to to create, read, update and delete notes. The notes object consists of the following attributes:

  • id: the unique id of a notes object
  • title: the title of the notes (required, minimum length 5 and maximum length 100 characters)
  • content: the actual content of the notes (required, minimum length 8 and maximum length 10,000 characters)

Where is the code?

The code for this post is available in this GitHub repository

Implementation

Notes object: add the Validation Rules

First we are going to add the validation rules as specified in the previous section. This can be done easily by adding the corresponding annotations from the javax.validation.constraints package. In this simple example we will use:

  • @NotNull: indicates required attributes
  • @Size: defines the boundaries for the element size (minimum and maximum number of characters)

If validation fails the validation framework will automatically create an error message that describes the cause of the error. We can customize this message by adding the message attribute to the annotation. A typical example looks like this:

@Size(min=8, max=10000, message="Error: minimum size is 8 and maximum is 10000")

Notes.java

package com.progressive.code.crud.domain;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * Created by abraun on 23/11/2017.
 */
@Entity
public class Notes {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NotNull
    @Size(min=5, max=100)
    private String title;

    @Column(length=10000)
    @NotNull
    @Size(min=8, max=10000, message="Error: minimum size is 8 and maximum is 10000")
    private String content;

    public Notes() {
    }

    public Notes(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

Controller: activate validation

To activate server-side validation we have to add the @Valid annotation to the corresponding attribute in the method signature:

@Valid Notes notes

Before the controller method is being invoked Spring performs the validation and provides the validation result in the BindingResult object. To get access to this object we have to add it to the method signature as well. The final method signature looks like this:

public String notesEdit(Model model, @Valid Notes notes, BindingResult bindingResult)

Additionally, we will add the logic to handle errors. First we have to check if the BindingResult includes errors (hasErrors method). If this is the case, we log the errors, add the notes object to the view model and return the view name "notesEdit". This allows us to show the form again using the notes object validation failed for.

If the BindingResult has no errors, we create or update the notes object in the database, retrieve the list of existing notes object and return to the list view.

DemoController.java

package com.progressive.code.crud.controller;

import com.progressive.code.crud.domain.Notes;
import com.progressive.code.crud.service.NotesService;

import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

/**
 * Created by abraun on 10/11/2017.
 */
@Controller
public class DemoController {

	private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    NotesService notesService;

    @RequestMapping(value="/")
    public String notesList(Model model) {
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

    @RequestMapping(value={"/notesEdit","/notesEdit/{id}"}, method = RequestMethod.GET)
    public String notesEditForm(Model model, @PathVariable(required = false, name = "id") Long id) {
        if (null != id) {
            model.addAttribute("notes", notesService.findOne(id));
        } else {
            model.addAttribute("notes", new Notes());
        }
        return "notesEdit";
    }

    @RequestMapping(value="/notesEdit", method = RequestMethod.POST)
    public String notesEdit(Model model, @Valid Notes notes, BindingResult bindingResult) {

        if(bindingResult.hasErrors()) {
            bindingResult.getAllErrors().forEach(err -> {
                LOGGER.info("ERROR {}", err.getDefaultMessage());
            });
            model.addAttribute("notes", notes);
            return "notesEdit";
        }

        notesService.saveNotes(notes);
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

    @RequestMapping(value="/notesDelete/{id}", method = RequestMethod.GET)
    public String notesDelete(Model model, @PathVariable(required = true, name = "id") Long id) {
        notesService.deleteNotes(id);
        model.addAttribute("notesList", notesService.findAll());
        return "notesList";
    }

}

View: show the errors

The last step is to show errors in the notes form to allow the user to correct the issues, based on the validation information we retrieved from the BindingResult.

The BindingResult provides the error information for each form element separately. Thymeleaf has access to the BindingResult and the #fields.hasErrors() method provides us the information if a particular form element has a validation error.

As we use Bootstrap 3 in this example, we can add the has-error CSS class to the form-group div if a validation error was detected. Adding this class clearly indicates validation errors by coloring the form element and the error text in red. A convenient way to do this is to use th:classappend with a conditional expression:

<div class="form-group" th:classappend="${#fields.hasErrors('notes.title')} ? 'has-error'">

Additonally, we want to show a text block that describes the validation error in detail. We use Thymeleaf's conditional th:if attribute to decide if we should render the error text.

<span th:if="${#fields.hasErrors('notes.title')}" th:errors="*{notes.title}" id="errorTitle" class="help-block"></span>

NotesEdit.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Notes</title>

    <link rel="stylesheet" type="text/css" media="all" href="../../css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" />

    <script type="text/javascript" th:src="@{/js/jquery.min.js}" src="js/jquery.min.js"></script>
    <script type="text/javascript" th:src="@{/js/bootstrap.min.js}" src="js/bootstrap.min.js"></script>
</head>
<body>
    <div class="container">

        <h1>Notes</h1>

        <form class="form-group" action="/notesEdit" th:action="@{/notesEdit}" method="post">
            <input th:type="hidden" name="id" th:field="${notes.id}" />
            <div class="form-group" th:classappend="${#fields.hasErrors('notes.title')} ? 'has-error'">
                <label for="title">Title</label>
                <input type="text" class="form-control" id="title" th:field="${notes.title}" />
                <span th:if="${#fields.hasErrors('notes.title')}" th:errors="*{notes.title}" id="errorTitle" class="help-block"></span>
            </div>
            <div class="form-group" th:classappend="${#fields.hasErrors('notes.content')} ? 'has-error'">
                <label for="content">Content</label>
                <textarea class="form-control" id="content" th:field="${notes.content}"></textarea>
                <span th:if="${#fields.hasErrors('notes.content')}" th:errors="*{notes.content}" id="errorContent" class="help-block"></span>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
            <a href="#" th:href="@{/}" class="btn btn-danger" role="button">Cancel</a>
        </form>

    </div>

</body>
</html>

Start and test the application

We can now start the application either from the IDE or from the command line.

Command Line

We can use

mvn package

to build the application from the command line and

java -jar target/CrudServerValidationApp-1.0-SNAPSHOT.jar

to start the application.

Test the application

After starting up we can access the application using this URL: localhost:8090

The screenshot below shows how the form looks like when validation errors occur.

Spring Boot server-side validation error

Code on GitHub

The code for this post is available in this GitHub repository