Alexander Lopatin
Alexander Lopatin

Reputation: 582

How can I to pass on additional parameters in validator for fields?

I have validation that email is unique. It's work during registration. But when I try to change email in user profile I have an error.

I check email to run userRepository.findByEmail. If the user was found then email isn't unique. But when the user changes his email in profile findByEmail returns this user. Validation fails.

I need to check that the user was returned by findByUser is not the same user that changes email. For it, I need to pass on the user that changes email in validator.

It's my code.

entity:

@Data
@Table(name = "users")
@NoArgsConstructor
@CheckPasswordConfirm(message = "Password not equal!")
@Entity
public class User implements UserDetails{

    private static final long serialVersionUID = 1L;

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

    @NotBlank(message = "Username is empty!")
    @UniqueUsername(message = "Username isn't unique")
    private String username;

    @NotBlank(message = "Password is empty!")
    private String password;

    @Transient
    @NotBlank(message = "Password confirm is empty!")
    private String passwordconfirm;

    private boolean active;

    @Email(message = "E-mail!")
    @NotBlank(message = "E-mail is empty!")
    @UniqueEmail(message = "Email isn't unique!")
    private String email;

    private String activationCode;

    @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
    @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    private Set<Role> roles;

    public boolean isAdmin()
    {
        return roles.contains(Role.ADMIN);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return getRoles();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Annotation UniqueEmail:

@Constraint(validatedBy = UniqueEmailValidator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface UniqueEmail {

    public String message();

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

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

}

Validator:

package ru.watchlist.domain.validation.validators;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.factory.annotation.Autowired;

import ru.watchlist.domain.validation.annotations.UniqueEmail;
import ru.watchlist.service.UserService;

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    @Autowired
    private UserService userService;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && !userService.isEmailAlredyUse(value);
    }

}

userService.isEmailAlredyUse:

    public boolean isEmailAlredyUse(String value) {
        User user = userRepository.findByEmail(value);
        if(user != null) {
            return true;
        }
        return false;
    }

Here I need check:

User userFromDB = userRepository.findByEmail(value);
        if(userFromDB != null && user != userFromDB) {
            return true;
        }

How can I solve this problem?

P.S. If I will do validator to class with cross fields, I can't show errors with their fields in Thymeleaf. Therefore I need validator for fields.

P.S.S. My controller:

@Controller
@RequestMapping("/profile")
public class ProfileController {

    @Autowired
    UserService userService;

    @GetMapping
    public String profile(@AuthenticationPrincipal User user, Model model) {

        model.addAttribute(user);
        return "profile";

    }

    @PostMapping("/update")
    public String saveChanges(@Valid User user, Errors errors) {

        if(errors.hasErrors()) {
            return "profile";
        }

        userService.addUser(user);

        return "redirect:/profile";
    }

}

UserRepository:

public interface UserRepository extends JpaRepository<User, Long>{

    User findByUsername(String username);

    User findByActivationCode(String code);

    User findByEmail(String email);

}

Upvotes: 0

Views: 3168

Answers (2)

Falcon
Falcon

Reputation: 468

User userFromDB = userRepository.findByEmail(value);
    if(userFromDB != null && user != userFromDB) {
        return true;
    }

In this case 'user' and 'userFromDB 'can always be different because this objects can have different hashCodes. Can you add implementation of your repository and class where user is returned? I assume You're not using JPA specification, but if you use it try to replace your own validation with this annatation @Column(unique = true)

If you want to validate your object in controller you can use @Valid annotation, where code from the controller's side can look like this:

 @PostMapping
public String saveObject(@Valid @ModelAttribute("email") Object object, BindingResult bindingResult) {

    if(bindingResult.hasErrors()){
        return view_with_form;
    }

    repository.save(object)
    return "redirect:path";
}

And from Thymeleaf form:

 <div class="col-md-6 mt-2 form-group">
                                <label>Recipe Description:</label>
                                <input type="text" class="form-control" th:field="*{email}" th:errorclass="is-invalid"/>
                                <span class="invalid-feedback" th:if="${#fields.hasErrors('email')}">
                                    <ul>
                                        <li th:each="error : ${#fields.errors('email')}" th:text="${error}"></li>
                                    </ul>
                                </span>
                            </div>

This example works with Bootstrap 4.

Upvotes: 0

Guillaume Smet
Guillaume Smet

Reputation: 10519

We don't recommend to use Hibernate Validator to check database constraints.

But if you want to do that, the idea is to use a constraint validator payload: https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#constraint-validator-payload . They have been created for this sort of requirements.

Example 6.7 is what you are looking for as you don't want to build a full ValidatorFactory each time, just a specific context.

Now, this is for the Hibernate Validator side but you will need a way to configure properly the Validator created by Spring to inject the logged in user in it. And I can't help you with this.

Upvotes: 0

Related Questions