Field Matching Bean Validation Annotation Example
This tutorial demonstrates a Field Matching Bean Validation Annotation Example. When you are building forms you may come across a requirement to validate/compare if different fields inside a form are equal to another field in the same form like password and/or email fields. In this example we build a simple form where we have a password and a confirmPassword field. We need to make sure the user has entered the correct password twice before submitting the request.
Project Structure
Let’s start by looking at the project structure.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.spring.security</groupId>
<artifactId>field-match</artifactId>
<version>1.0.0-SNAPSHOT</version>
<url>https://memorynotfound.com</url>
<name>Spring Security - ${project.artifactId}</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<!-- bootstrap and jquery -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.2.1</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Field Matching Bean Validation Annotation
We start by creating the FieldMatch
annotation. This is a class-level annotation where we can compare two fields for equality and pass in an optional message to display to the user if the constraint validation fails. We can also create a list of field matching annotations. This way we can validate field matching constraints multiple times.
package com.memorynotfound.spring.security.constraint;
import javax.validation.Payload;
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
String message() default "The fields must match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List
{
FieldMatch[] value();
}
}
The FieldMatch
annotation is validated by the FieldMatchValidator
. This class reads the two fields and the message during the initialization. The isValid()
method is invoked during bean validation. This method reads and compares the values of the two fields using commons-beanutils
.
When the first field doesn’t match the second field the validation fails and we add the error message to the conflicting property.
package com.memorynotfound.spring.security.constraint;
import org.apache.commons.beanutils.BeanUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
private String message;
@Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
message = constraintAnnotation.message();
}
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
boolean valid = true;
try
{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
valid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception ignore)
{
// ignore
}
if (!valid){
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(firstFieldName)
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
return valid;
}
}
Annotating Object With Bean Validator
Previously we created the @FieldMatch
annotation which we are now using in the PasswordResetDto
to validate if the password
field matches the confirmPassword
field. We optionally pass an message attribute which is displayed when the fields don’t match.
package com.memorynotfound.spring.security.web.dto;
import com.memorynotfound.spring.security.constraint.FieldMatch;
import org.hibernate.validator.constraints.NotEmpty;
@FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
public class PasswordResetDto {
@NotEmpty
private String password;
@NotEmpty
private String confirmPassword;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
}
Note: we can optionally create multiple field matching validators using the
@FieldMatch.List
annotation.@FieldMatch.List({ @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"), @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match") })
Processing Form Controller
We use Spring MVC to process the PasswordResetDto
form using the @Valid
annotation the bean validation is triggered automatically. When the form has encountered some errors, we return the user to the view.
package com.memorynotfound.spring.security.web;
import com.memorynotfound.spring.security.web.dto.PasswordResetDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
@Controller
@RequestMapping("/reset-password")
public class PasswordResetController {
@ModelAttribute("passwordResetForm")
public PasswordResetDto passwordReset() {
return new PasswordResetDto();
}
@GetMapping
public String showPasswordReset(Model model) {
return "reset-password";
}
@PostMapping
public String handlePasswordReset(@ModelAttribute("passwordResetForm") @Valid PasswordResetDto form,
BindingResult result) {
if (result.hasErrors()){
return "reset-password";
}
// save/updaate form here
return "redirect:/login?resetSuccess";
}
}
Spring Boot
We use Spring Boot to start our application.
package com.memorynotfound.spring.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Run {
public static void main(String[] args) {
SpringApplication.run(Run.class, args);
}
}
Thymeleaf Reset Form Template
The reset-password.html
thymeleaf template is located in the src/main/resources/templates
folder. The template uses boostrap
and jquery
loaded from the org.webjars
from Maven. It contains a simple form where the user has to enter two password fields. When these two fields match the form is validated.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
<title>Forgot Password</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<div class="text-center">
<h3><i class="glyphicon glyphicon-lock" style="font-size:2em;"></i></h3>
<h2 class="text-center">Reset password</h2>
<div class="panel-body">
<div th:if="${error}">
<div class="alert alert-danger">
<span th:text="${error}"></span>
</div>
</div>
<form th:action="@{/reset-password}" th:object="${passwordResetForm}" method="post">
<p class="error-message"
th:if="${#fields.hasGlobalErrors()}"
th:each="error : ${#fields.errors('global')}"
th:text="${error}">Validation error</p>
<input type="hidden" name="token" th:value="${token}"/>
<div class="form-group"
th:classappend="${#fields.hasErrors('password')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input id="password"
class="form-control"
placeholder="password"
type="password"
th:field="*{password}"/>
</div>
<p class="error-message"
th:each="error: ${#fields.errors('password')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group"
th:classappend="${#fields.hasErrors('confirmPassword')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input id="confirmPassword"
class="form-control"
placeholder="Confirm password"
type="password"
th:field="*{confirmPassword}"/>
</div>
<p class="error-message"
th:each="error: ${#fields.errors('confirmPassword')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-success">Reset password</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>
Demo
Access http://localhost:8080/reset-password
and fill in passwords that don’t match. You’ll receive the following error message.
Unit Testing
We used JUnit
to write the FieldMatchConstraintValidatorTest
Unit Test. This class tests the field matching annotation validator which we created earlier. First, we obtain a ValidatorFactory
which we use to retrieve a Validator
. Next we can create an instance of our PasswordResetDto
form and pass it to the validator.
package com.memorynotfound.spring.security.test;
import com.memorynotfound.spring.security.web.dto.PasswordResetDto;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
import static org.junit.Assert.assertEquals;
public class FieldMatchConstraintValidatorTest {
private static Validator validator;
@BeforeClass
public static void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
public void testValidPasswords() {
PasswordResetDto passwordReset = new PasswordResetDto();
passwordReset.setPassword("password");
passwordReset.setConfirmPassword("password");
Set<ConstraintViolation<PasswordResetDto>> constraintViolations = validator.validate(passwordReset);
assertEquals(constraintViolations.size(), 0);
}
@Test
public void testInvalidPassword() {
PasswordResetDto passwordReset = new PasswordResetDto();
passwordReset.setPassword("password");
passwordReset.setConfirmPassword("invalid-password");
Set<ConstraintViolation<PasswordResetDto>> constraintViolations = validator.validate(passwordReset);
assertEquals(constraintViolations.size(), 1);
}
}
Integration Testing
We use spring-test
and MockMvc
to write some integration tests.
This test validates a valid and invalid form submission.
package com.memorynotfound.spring.security.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringJUnit4ClassRunner.class)
public class PasswordResetIT {
@Autowired
private MockMvc mockMvc;
@Test
public void submitPasswordResetSuccess() throws Exception {
this.mockMvc
.perform(
post("/reset-password")
.param("password", "password")
.param("confirmPassword", "password")
)
.andExpect(model().hasNoErrors())
.andExpect(redirectedUrl("/login?resetSuccess"))
.andExpect(status().is3xxRedirection());
}
@Test
public void submitPasswordResetPasswordDoNotMatch() throws Exception {
this.mockMvc
.perform(
post("/reset-password")
.param("password", "password")
.param("confirmPassword", "invalid-password")
)
.andExpect(model().hasErrors())
.andExpect(model().attributeHasErrors("passwordResetForm"))
.andExpect(status().isOk());
}
}
References
- Spring MVC Reference Documenation
- ConstraintValidator JavaDoc
- ConstraintValidatorContext JavaDoc
- @Valid JavaDoc
Can i somehow show error message in jsp page without Thymeleaf?
Something like in spring forms:
<form:input path="confirmPassword">
<form:error path="confirmPassword" cssClass="error">
realy good job!!