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.

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());
    }

}

Download

上一篇: Spring Security In Memory Authentication Example
下一篇: Integrate Google ReCaptcha Java Spring Web Application
 评论 ( What Do You Think )
名称
邮箱
网址
评论
验证
   
 

 


  • 微信公众号

  • 我的微信

站点声明:

1、一号门博客CMS,由Python, MySQL, Nginx, Wsgi 强力驱动

2、部分文章或者资源来源于互联网, 有时候很难判断是否侵权, 若有侵权, 请联系邮箱:summer@yihaomen.com, 同时欢迎大家注册用户,主动发布无版权争议的 文章/资源.

3、鄂ICP备14001754号-3, 鄂公网安备 42280202422812号