Reputation: 582
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
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
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