Home > Blockchain >  Spring Validator validate method never called
Spring Validator validate method never called

Time:07-28

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 >
            <div >
                <security:authorize access="hasRole('ROLE_ADMIN')">
                <div  role="toolbar" aria-label="Toolbar with button groups">
                    <div  role="group" aria-label="Second group">
                        <a href="/users" type="button" >Back to users</a>
                    </div>
                </div>
                </security:authorize>
            </div><br>
        <form:form action="/users/save" method="post" modelAttribute="user">
            <div >
                <div >
                    <form:label path="id">User ID:</form:label><br>
                    <form:input path="id" type="UUID" readonly="true"  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"  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"  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"  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"  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"  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 >
                    <div  role="toolbar" aria-label="Toolbar with button groups">
                        <div  role="group" aria-label="Second group">
                            <form:button type="submit" value="Submit" >Save</form:button>
                        </div>
                    </div>
                </div>
            </div>
        </form:form>
        </div>
    </body>
</html>

CodePudding user response:

If you look at the classname from your debugger com.intellias.testmarketplace.controller.UserController$$EnhancerBySpringCGLIB$$309171b7 you see the $$EnhancerBySpringCGLIB. That part indicates that a proxy is being generated by Spring. A proxy can be generated for various reasons but the most common one for controllers is security, the @PreAuthorize annotation comes to mind. Another option is the @Timed annotation to do monitoring on the controller.

As your initBinder method is private this will be invoked directly on the proxy and not on the actual bean instance. This is due to a class proxy being created, which works by extending your class. A method is being generated with the same signature and the interceptor is invoked and then the call is passed on to the actual method. However this will only work for public and protected methods not for private or final methods. Those will be directly invoked on the empty proxy (the fields are null as that doesn't need the dependencies).

As your method is private it is invoked on the proxy. The proxy doesn't have the fields set, so the userValidator is null. So effectivly there is no Validator being set on the WebDataBinder and thus no validation will and can be done.

To fix, make the method public (or protected) so that it will properly be called on the actual bean and not the proxy.

For the future and as a rule of thumb you probably want your methods with annotations in a controller to be public anyway instead of `private.

CodePudding user response:

The issue was in private access modifier for initBinder method combined with @PreAuthorize annotation on some UserController methods. Many thanks to @M.Deinum for patience and help.

  • Related