I am developing basic reset password flow. This is my DTO:
@Getter
@Setter
@FieldsValueMatch(field = "password", fieldMatch = "confirmPassword", message = "Passwords do not match")
public class PasswordResetForm {
@Size(min = 8, message = "Password needs to have at least 8 characters")
private String password;
private String confirmPassword;
}
There is FieldsValueMatch
annotation:
@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {
String message() default "Fields values don't match!";
String field();
String fieldMatch();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And validator:
public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, Object> {
private String field;
private String fieldMatch;
@Override
public void initialize(FieldsValueMatch constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);
if (fieldValue != null) {
return fieldValue.equals(fieldMatchValue);
} else {
return fieldMatchValue == null;
}
}
}
This is my controller:
@Controller
public class ResetPasswordController {
@ModelAttribute("passwordResetForm")
public PasswordResetForm passwordResetForm() {
return new PasswordResetForm();
}
@GetMapping("/reset-password")
public String showResetPasswordForm(final Model model) {
return "reset-password";
}
@PostMapping("/reset-password-result")
public String resetPassword(@Valid final PasswordResetForm passwordResetForm,
final BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "reset-password";
}
// reset password logic
return "redirect:/reset-password-success";
}
}
and part of Thymeleaf page:
<form th:action="@{/reset-password-result}" th:object="${passwordResetForm}" method="post">
<div>
<div class="input-group">
<input id="password"
class="form-input"
placeholder="Set a new password"
type="password"
th:field="*{password}"/>
</div>
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
</div>
<div>
<div class="input-group">
<input id="confirmPassword"
class="form-input"
placeholder="Re-type a new password"
type="password"
th:field="*{confirmPassword}"/>
</div>
<div th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
</div>
<div class="form-group">
<button type="submit" class="form-btn">SET</button>
</div>
</form>
And now, when I enter different passwords in both inputs, I get the following message in terminal:
2021-03-26 11:13:39.315 WARN 1340 [nio-8080-exec-7]
s.w.s.h.AbstractHandlerExceptionResolver : Resolved
[org.springframework.validation.BindException:
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Error in object 'passwordResetForm': codes
[FieldsValueMatch.passwordResetForm,FieldsValueMatch]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [passwordResetForm.,]; arguments []; default message [],password,confirmPassword]; default message [Passwords do not match]]
and instant 400 result code. My resetPassword
method in the Controller is not called, so instead my Thymeleaf page I get Whitelabel Error Page. The same happens when I put password shorter than 8 characters. What am I doing wrong?
I'd appreciate your help!
Try to add @Validated
annotation on ResetPasswordController
class (before/after @Controller
annotation). This should enabled validation
You define @FieldsValueMatch
as a class-level constraint. Probably the generated default constraint violation is causing the problem, since in this case no explicit property path is specified for the created constraint violation.
The same happens when I put password shorter than 8 characters.
Regardless of the other field-level validation (= @Size
), the @FieldsValueMatch
will be executed in any case, which is probably why you are still facing the same problem.
So, adjusting the FieldsValueMatchValidator
implementation - by setting the property path for the created constraint violation and providing a custom error message - should fix the problem:
public class FieldsValueMatchValidator implements
ConstraintValidator<FieldsValueMatch, Object> {
// ...
private String message;
@Override
public void initialize(FieldsValueMatch constraintAnnotation) {
// ...
this.message = constraintAnnotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//...
boolean valid;
if (fieldValue != null) {
valid = fieldValue.equals(fieldMatchValue);
} else {
valid = fieldMatchValue == null;
}
if (!valid){
context.buildConstraintViolationWithTemplate(this.message) // setting the custom message
.addPropertyNode(this.field) // setting property path
.addConstraintViolation() // creating the new constraint violation
.disableDefaultConstraintViolation() // disabling the default constraint violation
;
}
return valid;
}
}
Check your method arguments sequence.
You must declare an
Errors
, orBindingResult
argument immediately after the validated method argument.
So, if your code actually looks like following, your problem will be reproduced.
@PostMapping("/reset-password-result")
public String resetPassword(@Valid PasswordResetForm passwordResetForm, Model model,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "reset-password";
}
// reset password logic
return "redirect:/reset-password-success";
}
thanks for all the replies. For no reason, it just started to work without any change from my side... I don't know what happened, maybe I needed to restart my web browser or something like this...
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.