본문 바로가기

Backend/Java

Spring Boot Form

반응형

Spring Boot provides an easy way to manipulate the form data. Let's see how we can bind, validate, and send form data.

List of Contents

Using Fields

We can send the data in the template as shown below (name is the key)

<form th:action="@{/formSubmit} method="GET">
  <input type="text" name="userName" />
</form>

There are two ways to get the sent data

Using getParameter

@RequestMapping("/resultForm")
public String processForm(HttpServletRequest request, Model model) {
  String name = request.getParameter("userName")
  model.addAttribute("name", name);
  return "formResult";
}

Using @RequestParam

@RequestMapping("/resultForm")
public String processForm(@RequestParam("userName") String name, Model model) {  
  model.addAttribute("name", name);
  return "formResult";
}

Using Form Object

This uses the setter when a form is submitted and the getter when the form is received to set and get the data

Entity Class

Create an entity class. Add no parameter constsructor, getter, and setter

Adding Object and Field in the Controller, Linking to a Template

@GetMapping("/userForm")
public String displayForm(Model model) {
    User user = new User();
    model.addAttribute("user", user);
    
    return "user-form";
}

Creating a Form

Add a form in the template file and bind the object

<form th:action="@{/showResult}" th:object="${user}" method="POST">
</form>

Adding Form Fields

▶ Text

<input type="text" th:field="*{firstName}">

▶ Select

<select th:field="*{gender}">-->
    <option th:value="Male">Male</option>-->
    <option th:value="Female">Female</option>-->
</select>

▶ Radio

<input type="radio" th:field="*{level}" th:value="high">
<input type="radio" th:field="*{level}" th:value="low">

▶ Checkbox

<input type="checkbox" th:field="*{hobbies}" th:value="nap"/>
<input type="checkbox" th:field="*{hobbies}" th:value="excercise"/>
<input type="checkbox" th:field="*{hobbies}" th:value="'going home'"/>

For the fields that use multiple values such as the select or radio, we can use dynamic values with environment variables using the application.properties file

Add the environment variables as a property to the controller

@Value("${genders}")
private List<String> genders;

@Value("${levels}")
private List<String> levels;

@Value("${hobbies}")
private List<String> hobbies;
    
@GetMapping("/userForm")
public String displayForm(Model model) {
    User user = new User();
    model.addAttribute("user", user);

    model.addAttribute("genders", genders);
    model.addAttribute("levels", levels);
    model.addAttribute("hobbies", hobbies);
    return "user-form";
}

▶ Select (Using env variables)

<select th:field="*{gender}">
    <option th:each="gender:${genders}" th:value="${gender}" th:text="${gender}"></option>
</select>

▶ Radio (Using env variables)

<input type="radio" th:field="*{level}" th:each="level:${levels}" th:value="${level}" th:text="${level}">

▶ Checkbox (Using env variables)

<input type="checkbox" th:field="*{hobbies}" th:each="hobby:${hobbies}" th:value="${hobby}" th:text="${hobby}"/>

Sending Data

▶ Controller

@PostMapping("/showResult")
public String showResult(@ModelAttribute("user") User user, BindingResult bindingResult, Model model) {    
    return "user-confirmation";
}

▶ Form Result

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>Result</h3>
<p th:text="${user.firstName}">
<p th:text="${user.gender}">
<p th:text="${user.level}">
<!--<p th:text="${user.hobbies}">-->
<ul>
    <li th:each="hobby:${user.hobbies}" th:text="${hobby}"/>
</ul>
</body>
</html>

Completed Codes

▶ Entity

package com.example.form.entity;

import java.util.List;

public class User {
    private String firstName;
    private String lastName;
    private String gender;
    private String level;
    private List<String> hobbies;

    public User() {
    }

    public List<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

▶ Controller

package com.example.form.controller;

import com.example.form.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class UserController {
    @Value("${genders}")
    private List<String> genders;

    @Value("${levels}")
    private List<String> levels;

    @Value("${hobbies}")
    private List<String> hobbies;

    @GetMapping("/userForm")
    public String displayForm(Model model) {
        User user = new User();
        model.addAttribute("user", user);

        model.addAttribute("genders", genders);
        model.addAttribute("levels", levels);
        model.addAttribute("hobbies", hobbies);
        return "user-form";
    }

    @PostMapping("/showResult")
    public String showResult(@ModelAttribute("user") User user, BindingResult bindingResult, Model model) {
        System.out.println("bindingResult = " + bindingResult.toString());
        return "user-confirmation";
    }
}

▶ Sending Form

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>Form</h3>
<form th:action="@{/showResult}" th:object="${user}" method="POST">
    <input type="text" th:field="*{firstName}">
    <input type="text" th:field="*{lastName}">
    <!--    <select th:field="*{gender}">-->
    <!--        <option th:value="Male">Male</option>-->
    <!--        <option th:value="Female">Female</option>-->
    <!--    </select>-->
    <select th:field="*{gender}">
        <option th:each="gender:${genders}" th:value="${gender}" th:text="${gender}"></option>
    </select>

    <!--    <input type="radio" th:field="*{level}" th:value="high">-->
    <!--    <input type="radio" th:field="*{level}" th:value="low">-->
    <input type="radio" th:field="*{level}" th:each="level:${levels}" th:value="${level}" th:text="${level}">

    <!--    <input type="checkbox" th:field="*{hobbies}" th:value="nap"/>-->
    <!--    <input type="checkbox" th:field="*{hobbies}" th:value="excercise"/>-->
    <!--    <input type="checkbox" th:field="*{hobbies}" th:value="'going home'"/>-->
    <input type="checkbox" th:field="*{hobbies}" th:each="hobby:${hobbies}" th:value="${hobby}" th:text="${hobby}"/>

    <input type="submit" value="Submit">
</form>
</body>
</html>

▶ Form Result

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>Result</h3>
<p th:text="${user.firstName}">
<p th:text="${user.gender}">
<p th:text="${user.level}">
<!--<p th:text="${user.hobbies}">-->
<ul>
    <li th:each="hobby:${user.hobbies}" th:text="${hobby}"/>
</ul>
</body>
</html>

Validations

Dependencies

Add the dependency for the validation to the pom.xml file

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Adding Validation in Entity

Add validations to the entity class

▶ Mandatory

@NotNull(message = "required")
private String lastName;

▶ Size

@Size(min = 1, max = 3, message = "Must be more than 1 and less than 3")
private String lastName;

▶ Min

@Min(value = 0, message = "over 1")
private int level;

▶ Max

@Max(value = 10, message = "less than 10")
private int level;

▶ Regex

@Pattern(regexp = "$[a-zA-Z0-9]{5}", message = "format")
private String postal;

Showing Error Messages

We can show the error messages using the code shown below

th:if="${#fields.hasErrors('<fieldName>')}" th:errors="*{fieldName}"
th:if="${#fields.hasErrors('<fieldName>')}" th:errors="${user.fieldName}"
<form th:action="@{/resultForm}" th:object="${user}" method="POST">
    <input type="text" th:field="*{firstName}"/>
    <input type="text" th:field="*{lastName}"/>
    <span th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>

    <input type="text" th:field="*{level}">
    <span th:if="${#fields.hasErrors('level')}" th:errors="*{level}"></span>

    <input type="text" th:field="*{postal}">
    <span th:if="${#fields.hasErrors('postal')}" th:errors="*{postal}"></span>

    <input type="text" th:field="*{code}">
    <span th:if="${#fields.hasErrors('code')}" th:errors="*{code}"></span>

    <input type="submit" value="Submit"/>
</form>

Custom Validations

Create a package then add an annotation below that

In the annotation created, add the annotations shown below. Note that we have to add the validation class used in the @Constraint

package com.example.formval.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = CodeValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Code {
    public String value() default "CD";

    public String message() default "start with CD";

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

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

Add the validation class and inherit from the below interface

ConstraintValidator<<Annotation>, <Type>>

For example, to check if a value starts with a certain value, we can write a code as follows. The first method sets the entered value and the second method validates and returns a boolean

package com.example.formval.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class CodeValidator implements ConstraintValidator<Code, String> {
    private String prefix;

    @Override
    public void initialize(Code code) {
        prefix = code.value();
    }

    @Override
    public boolean isValid(String code, ConstraintValidatorContext constraintValidatorContext) {
        boolean result = true;
        if (code != null) {
            result = code.startsWith(prefix);
        }
        return result;
    }
}

▶ Using the Default Value

@Code
private String code;

▶ Overriding the Default Value

@Code(value = "CDO")
private String code;

We can see the errors thrown using the BindingResult

@PostMapping("/resultForm")
public String resultForm(@Valid @ModelAttribute("user") User user,
            BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        System.out.println(bindingResult.toString());
        return "user-form";
    } else {
        return "confirmation";
    }
}

And using the property names in the codes array (The further right the less specific)

We can change the error messages in the messages.properties

typeMismatch.user.level=Invalid Number

Completed Codes

▶ Validation Annotation

package com.example.formval.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = CodeValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Code {
    public String value() default "CD";

    public String message() default "start with CD";

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

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

▶ Validator Class

package com.example.formval.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class CodeValidator implements ConstraintValidator<Code, String> {
    private String prefix;

    @Override
    public void initialize(Code code) {
        prefix = code.value();
    }

    @Override
    public boolean isValid(String code, ConstraintValidatorContext constraintValidatorContext) {
        boolean result = true;
        if (code != null) {
            result = code.startsWith(prefix);
        }
        return result;
    }
}

▶ Entity

package com.example.formval.entity;

import com.example.formval.validation.Code;
import jakarta.validation.constraints.*;

public class User {
    private String firstName;

    @NotNull(message = "required")
    @Size(min = 1, max = 3, message = "Must be more than 1 and less than 3")
    private String lastName;

    @Min(value = 0, message = "over 1")
    @Max(value = 10, message = "less than 10")
    private int level;

    @NotNull
    private String levelMen;

    @Pattern(regexp = "$[a-zA-Z0-9]{5}", message = "format")
    private String postal;

    @Code(value = "CDO")
    private String code;

    public User() {
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getLevelMen() {
        return levelMen;
    }

    public void setLevelMen(String levelMen) {
        this.levelMen = levelMen;
    }

    public String getPostal() {
        return postal;
    }

    public void setPostal(String postal) {
        this.postal = postal;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

▶ Controller

package com.example.formval.controller;

import com.example.formval.entity.User;
import jakarta.validation.Valid;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class UserController {
    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {
        StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
        dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
    }

    @GetMapping("/")
    public String showForm(Model model) {
        model.addAttribute("user", new User());
        return "user-form";
    }

    @PostMapping("/resultForm")
    public String resultForm(
            @Valid @ModelAttribute("user") User user,
            BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            System.out.println(bindingResult.toString());
            return "user-form";
        } else {
            return "confirmation";
        }
    }
}

▶ Sending Form

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form th:action="@{/resultForm}" th:object="${user}" method="POST">
    <input type="text" th:field="*{firstName}"/>
    <input type="text" th:field="*{lastName}"/>
    <span th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>

    <input type="text" th:field="*{level}">
    <span th:if="${#fields.hasErrors('level')}" th:errors="*{level}"></span>

    <input type="text" th:field="*{postal}">
    <span th:if="${#fields.hasErrors('postal')}" th:errors="*{postal}"></span>

    <input type="text" th:field="*{code}">
    <span th:if="${#fields.hasErrors('code')}" th:errors="*{code}"></span>

    <input type="submit" value="Submit"/>
</form>
</body>
</html>

▶ Form Result

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${user.firstName + ' ' + user.lastName + ' ' + user.level + ' ' + user.postal + ' ' + user.code"/>
</body>
</html>

In this writing, we have seen Spring Boot form and how we can use it.


References

Jakarta Bean Validation - Home

 

Jakarta Bean Validation - Home

Jakarta Bean Validation lets you express constraints on object models via annotations lets you write custom constraints in an extensible way provides the APIs to validate objects and object graphs provide the APIs to validate

beanvalidation.org

 

728x90
반응형

'Backend > Java' 카테고리의 다른 글

Class  (0) 2023.12.28
Switch  (0) 2023.12.26
Spring Beans - Dependency Injection  (1) 2023.10.07
Spring Boot Security  (1) 2023.10.01
Spring Boot Actuator  (0) 2023.10.01