java – Spring Validator validate method never called

I have two close to each other controllers – one for products and one for users. I’d like to add Spring Validation to both controllers. So I added ProductValidator and UserValidator classes which implement Validator. ProductValidator works fine. The problem is UserValidator is never called. InitBinder method is being called, but validation doesn’t work. At the same time everything works fine with products in ProductController.

Here’s the code of user logic.

UserController

@Controller
@RequestMapping(path = "/users")
public class UserController {
    private final UserService userService;
    private final RoleService roleService;
    private PasswordEncoder passwordEncoder;

    @Autowired
    public UserController(UserService userService, RoleService roleService, PasswordEncoder passwordEncoder) {
        this.userService = userService;
        this.roleService = roleService;
        this.passwordEncoder = passwordEncoder;
    }

    @Autowired
    @Qualifier("userValidator")
    private UserValidator userValidator;

    @InitBinder("user")
    private void initBinder(WebDataBinder binder) {
        binder.setValidator(userValidator);
        binder.registerCustomEditor(Role.class, "roles", new RoleEditor(roleService));
    }

    ...

    @RequestMapping(path = "/save", method = RequestMethod.POST)
    public ModelAndView submit(@ModelAttribute("user") @Valid User user,
                         BindingResult result) {
        ...
    }
...
}

UserValidator

@Component("userValidator")
public class UserValidator implements Validator {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    @Autowired
    public UserValidator(UserRepository userRepository, RoleRepository roleRepository) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
    }

    @Override
    public boolean supports(Class<?> paramClass) {
        return User.class.equals(paramClass);
    }

    @Override
    public void validate(Object obj, Errors errors) {

        User user = (User) obj;

        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "username.required", "Enter username");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "firstName.required", "Enter user first name");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "lastName.required", "Enter user last name");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "password.required", "Enter user password");

        User userToFind = userRepository.findByUsername(user.getUsername()).orElse(new User());

        if (Objects.isNull(user.getId()) && Objects.nonNull(userToFind.getId())) {
            errors.rejectValue("username", "username.already.exist", "user with this username already exists");
        }

        if (Objects.isNull(user.getRoles())) {
            errors.rejectValue("roles", "role.required", "role must be assigned");

        }

        Role adminRole = roleRepository.getAdminRole();
        if (Objects.nonNull(user.getId())
                && Objects.nonNull(user.getRoles())
                && !user.getRoles().contains(adminRole)
                && userRepository.findUsersWithAdministratorRole().size() == 1) {
            errors.rejectValue("roles", "admin.required", "at least one admin role must exist");
        }
    }

    public ErrorMessage validateUserToDelete(UUID id) {
        ErrorMessage errorMessage = new ErrorMessage();
        List<String> errors = new ArrayList<>();
        Set<User> admins = userRepository.findUsersWithAdministratorRole();

        if (admins.size() == 1) {
            errors.add(String.format("User with email %s is the one with Admin role. Impossible to delete last Admin user."
                    , userRepository.findById(id).get().getUsername()));
        }
        errorMessage.setErrors(errors);
        return errorMessage;
    }
}

User

@Entity
@Table(name = "users")
public class User {
    private UUID id;
    private String username;
    private String password;
    private String firstName;
    private String lastName;
    private BigDecimal money;
    private Set<Role> roles;
    private Set<Product> products;

    @Id
    @Type(type="org.hibernate.type.PostgresUUIDType")
    @Column(name = "id", columnDefinition = "uuid")
    @GeneratedValue(strategy = GenerationType.AUTO)
    public UUID getId() {
        return id;
    }

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

    @Column(name = "username")
    @NotEmpty
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(name = "password")
    @NotEmpty
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Column(name = "first_name")
    @NotEmpty
    public String getFirstName() {
        return firstName;
    }

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

    @Column(name = "last_name")
    @NotEmpty
    public String getLastName() {
        return lastName;
    }

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

    @Column(name = "money")
    @DecimalMin(value = "0.0", inclusive = false)
    @Digits(integer = 10, fraction = 2)
    public BigDecimal getMoney() {
        return money;
    }
    public void setMoney(BigDecimal money) {
        this.money = money;
    }

    @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinTable(
            name = "users_roles",
            joinColumns = { @JoinColumn(name = "user_id") },
            inverseJoinColumns = { @JoinColumn(name = "role_id") }
    )
    @NotNull
    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinTable(
            name = "checkout",
            joinColumns = { @JoinColumn(name = "user_id") },
            inverseJoinColumns = { @JoinColumn(name = "product_id") }
    )
    public Set<Product> getProducts() {
        return products;
    }

    public void setProducts(Set<Product> products) {
        this.products = products;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return id.equals(user.id) && username.equals(user.username) && password.equals(user.password) && firstName.equals(user.firstName) && lastName.equals(user.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, password, firstName, lastName);
    }
}

user.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
    <head>
         <c:import url="${contextPath}/WEB-INF/jsp/header.jsp"/>
    </head>

    <body>

        <c:import url="${contextPath}/WEB-INF/jsp/navibar.jsp"/>
        <div class="container">
            <div class="row">
                <security:authorize access="hasRole('ROLE_ADMIN')">
                <div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
                    <div class="btn-group me-2" role="group" aria-label="Second group">
                        <a href="/users" type="button" class="btn btn-success">Back to users</a>
                    </div>
                </div>
                </security:authorize>
            </div><br>
        <form:form action="/users/save" method="post" modelAttribute="user">
            <div class="form-group">
                <div class="row">
                    <form:label path="id">User ID:</form:label><br>
                    <form:input path="id" type="UUID" readonly="true" class="form-control" id="id" placeholder="User ID" name="id" value="${user.id}"/>
                    <br>
                    <form:label path="username">username (email address):</form:label><br>
                    <form:input path="username" type="text" class="form-control" id="username" placeholder="Enter username" name="username" value="${user.username}"/>
                    <form:errors path="username" cssClass="error"/>
                    <br>
                    <form:label path="password">Password:</form:label><br>
                    <form:input path="password" type="text" class="form-control" id="password" placeholder="Enter password" name="password" value="${user.password}"/>
                    <form:errors path="password" cssClass="error"/>
                    <br>
                    <form:label path="firstName">First name:</form:label><br>
                    <form:input path="firstName" type="text" class="form-control" id="firstName" placeholder="Enter first name" name="firstName" value="${user.firstName}"/>
                    <form:errors path="firstName" cssClass="error"/>
                    <br>
                    <form:label path="lastName">First name:</form:label><br>
                    <form:input path="lastName" type="text" class="form-control" id="lastName" placeholder="Enter last name" name="lastName" value="${user.lastName}"/>
                    <form:errors path="lastName" cssClass="error"/>
                    <br>
                    <form:label path="money">Money:</form:label><br>
                    <form:input path="money" type="number" class="form-control" id="money" placeholder="Enter money" name="money" value="${user.money}"/>
                    <form:errors path="money" cssClass="error"/>
                    <br>
                    <security:authorize access="hasRole('ROLE_ADMIN')">
                    <form:label path="roles">Roles:</form:label><br>
                    <c:forEach items="${roles}" var="role">
                        <form:checkbox path="roles" id="${roles}" label="${role.name}" value="${role}"/></td>
                    </c:forEach>
                    <br>
                    <form:errors path="roles" cssClass="error"/><br><br>
                    </security:authorize>
                </div>
                <div class="row">
                    <div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
                        <div class="btn-group me-2" role="group" aria-label="Second group">
                            <form:button type="submit" value="Submit" class="btn btn-primary">Save</form:button>
                        </div>
                    </div>
                </div>
            </div>
        </form:form>
        </div>
    </body>
</html>

Leave a Comment