I have following annotation to validate password:
@Target({FIELD}) @Retention(RUNTIME) @Documented @NotNull @Length(min = 8, max = 32) @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\S+$).{8,}$") public @interface Password { Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
But spring validation does not recognize this rules. I used this annotation as:
@Password private String password;
How can I get it without defining ConstraintValidator
instance?
Advertisement
Answer
If you want to use ConstraintValidator
, you can do it like this:
create Password annotation :
@Documented @Constraint(validatedBy = PasswordConstraintValidator.class) @Target({ FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) public @interface Password { String message() default "{propertyPath} is not a valid password"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
then create the PasswordConstraintValidator class :
public class PasswordConstraintValidator implements ConstraintValidator<Password, String> { private final String PASSWORD_PATTERN = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#&()–[{}]:;',?/*~$^+=<>]).{8,20}$"; private final Pattern pattern = Pattern.compile(PASSWORD_PATTERN); @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(Objects.isNull(value)) { return false; } if((value.length() < 8) || (value.length() > 32)) { return false; } if(!pattern.matcher(password).matches()){ return false; } }
Then apply it to one of your fields, note that you can also put a custom message:
@Password(message = "....") private String password; @Password private String passwd;
You can also refactor the if statements each in an appropriate method (to have a clean code): something that will look like this :
@Override public boolean isValid(String value, ConstraintValidatorContext context) { return (notNull(value) && isValidPasswordLength(value) && isValidPasswordValue(value)); }
Update
since you don’t want to use the ConstraintValidator
, your implementation looks fine, you just need to add @Valid
on your model so that cascading validation can be performed and include spring-boot-starter-validation
to make sure that validation api is included and add @Constraint(validatedBy = {})
on your custom annotation. Here is a groovy example here (you can run it with spring CLI) :
@Grab('spring-boot-starter-validation') @Grab('lombok') import lombok.* @Grab('javax.validation:validation-api:2.0.1.Final') import javax.validation.constraints.NotNull import javax.validation.constraints.Size import javax.validation.Valid import javax.validation.Constraint import javax.validation.Payload import java.lang.annotation.Documented import java.lang.annotation.Target import java.lang.annotation.Retention import static java.lang.annotation.ElementType.FIELD import static java.lang.annotation.RetentionPolicy.RUNTIME @RestController class TestCompositeAnnotation { @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public String register(@Valid @RequestBody User user) { return "password " + user.password + " is valid"; } } class User { public String username; @Password public String password; } @Target(value = FIELD) @Retention(RUNTIME) @Documented @NotNull @Constraint(validatedBy = []) // [] is for groovy make sure to replace is with {} @Size(min = 8, max = 32) @interface Password { String message() default "invalid password"; Class<?>[] groups() default []; // [] is for groovy make sure to replace is with {} Class<? extends Payload>[] payload() default []; // [] is for groovy make sure to replace is with {} }
So when you curl :
curl -X POST http://localhost:8080/register -d '{"username": "rsone", "password": "pa3"}' -H "Content-Type: application/json"
you will get an error validation response :
{"timestamp":"2020-11-07T16:43:51.926+00:00","status":400,"error":"Bad Request","message":"...","path":"/register"}