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?
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"}
Related
I have User class with list of roles:
#Data
#Table(name = "users")
#Entity
public class User {
String username;
String password;
List<Role> roles;
}
And the role enum:
public enum Role {
Admin,User,Manager
}
I need to validate the bean before insert it to DB. The validation expression should look like this:
long count = user.getRoles().stream().filter(r -> r.equals(Role.Manager)).count();
!(count > 1);
User cant have more than one manager role, but for other roles its ok to have duplicates;
For this i created a custom constraint and a validator for him:
Constraint:
#Target({ ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = RoleValidator.class)
public #interface RoleConstraint {
String message() default "error";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Validator:
public class RoleValidator implements ConstraintValidator<RoleConstraint, List<Role>> {
#Override
public boolean isValid(List<Role> roles, ConstraintValidatorContext context) {
long count = roles.stream().filter(r -> r.equals(Role.Manager)).count();
return !(count > 1);
}
}
but it doesnt work. I also found a solution here but i cant wrap the list in beanList because a lot of other classes depends on this class. Is there other way to solve this problem. Any suggestion is acceptable
Since you are validating the User, you can make you annotation work with a user. Have the annotation work on classes:
#Target({ ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = RoleValidator.class)
public #interface RoleConstraint {
String message() default "error";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Change the validator to work with user:
public class RoleValidator implements ConstraintValidator<RoleConstraint, User> {
#Override
public boolean isValid(User user, ConstraintValidatorContext context) {
long count = user.roles.stream().filter(r -> r.equals(Role.Manager)).count();
//that's simpler, but is the same as '!(count > 1)'
return count <= 1;
}
}
Then apply #RoleConstraint on User class.
I am trying to validate my rest request according to some fields existance. For example, if transactionDate field is null or didnt exist in my request object, I want to throw an error to client.
I couldn't do it despite the source of this guide and still my requests can pass in controller.
How can I validate two or more fields in combination?
DTO
#FraudRestRequestValidator
public class FraudActionsRestRequest {
private BigDecimal amount;
private String receiverTransactionDate;
private String receiverNameSurname;
private BigDecimal exchangeRate;
private String transactionReferenceNumber;
#NotNull
private String transactionDate;
#NotNull
private String transactionTime;
private String transactionTimeMilliseconds;
private BigDecimal tlAmount;
private String channel;
}
ANNOTATION
#Constraint(validatedBy = FraudActionsRestValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface FraudRestRequestValidator {
String message() default "Invalid Limit of Code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
VALIDATOR
public class FraudActionsRestValidator implements ConstraintValidator<FraudRestRequestValidator, FraudActionsRestRequest> {
#Override
public void initialize(FraudRestRequestValidator constraintAnnotation) {
}
#Override
public boolean isValid(FraudActionsRestRequest fraudActionsRestRequest, ConstraintValidatorContext constraintValidatorContext) {
//I will implement my logic in future
return false;
}
}
REST CONTROLLER
#PostMapping("/getFraudActions")
public ResponseEntity<?> getFraudActions(#Valid #RequestBody FraudActionsRestRequest fraudActionsRestRequest, Errors errors) throws Exception
Thanks.
In your custom validator just implement logic you want to have. You did everything correct except some minor thing:
#Constraint(validatedBy = FraudActionsRestValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidFraudRestRequest {
String message() default "Invalid Limit of Code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class FraudActionsRestValidator implements ConstraintValidator<ValidFraudRestRequest, FraudActionsRestRequest> {
#Override
public void initialize(ValidFraudRestRequest constraintAnnotation) {
}
#Override
public boolean isValid(FraudActionsRestRequest fraudActionsRestRequest, ConstraintValidatorContext constraintValidatorContext) {
return fraudActionsRestRequest.getTransactionDate() != null && fraudActionsRestRequest.getTransactionTime() != null && additional check you need;
}
}
Looks all okaish.
You might be missing the #Validated annotation on the rest controller class,
See https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-validation.html for more info
There is a custom validation annotation created to check if two spring form fields are equal or not.
PasswordVerification:
#Constraint(validatedBy = PasswordVerificationValidator.class)
#Target({ElementType.TYPE, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface PasswordVerification {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PasswordVerificationValidator:
public class PasswordVerificationValidator implements ConstraintValidator<PasswordVerification, UserFormRegistration> {
#Override
public void initialize(PasswordVerification constraintAnnotation) {}
#Override
public boolean isValid(UserFormRegistration userFormRegistration, ConstraintValidatorContext context) {
return userFormRegistration.getPassword().equals(userFormRegistration.getVerifyPassword());
}
}
UserFormRegistration:
#PasswordVerification(message = "Password and password confirmation fields don't match")
public class UserFormRegistration {
private String password;
...
So, if the annotation is applied to the class UserFormRegistration, it works fine. But if I want to apply it to the field (see below), it fails.
public class UserFormRegistration {
#PasswordVerification(message = "Password and password confirmation fields don't match")
private String password;
...
Exception:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'ua.com.vertex.validators.interfaces.PasswordVerification' validating type 'java.lang.String'. Check configuration for 'password'
How to fix?
I guess you want to apply the annotation at method level also so you need to have ElementType.METHOD
so change #Target({ElementType.TYPE, ElementType.FIELD}) to
#Target({ElementType.TYPE, ElementType.FIELD,ElementType.METHOD})
so now #PasswordVerification will be applicable to methods, classes,interfaces,enums and fields
I use javax.validation.constraints annotations to validate the request parameters. I need to validate emails so I've created an annotation #EmailValid, snippet provided below:
#Email(message = "Please provide a valid email address")
#Pattern(regexp = ".+#.+\\..+", message = "Please provide a valid email address")
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = {})
#Documented
public #interface EmailValid {
String message() default "Please provide a valid email address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
But it works only for single String field e.g. :
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmail", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmail(#EmailValid String email) {
// some code for sending email
}
What I need is to validate Map of emails. I have a map Map<String, String> users where key is userId and value is an email. So I have a method like this in my controller:
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmails", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmails(Map<String, String> users) {
// some code for sending email
}
Question is how can I validate map values here using my annotation #EmailValid and other javax.validation.constraints annotations?
You have to implement custom validation like below.
First Change your EmailValid.java file.
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = { EmailValidCheck.class })
#Documented
public #interface EmailValid {
String message() default "Please provide a valid email address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Now Implement This ConstraintValidator like Below.
public class EmailValidCheck extends ABaseController implements
ConstraintValidator<EmailValid, Map<String,String>> {
#Override
public void initialize(EmailValid constraintAnnotation) {
}
#Override
public boolean isValid(Map<String,String> mapOfEmail,
ConstraintValidatorContext cvc) {
//Implement Email Validation Login Here.
}
}
Change Your Controller Like Below.
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmails", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmails(#EmailValid Map<String, String> users) {
// some code for sending email
}
I am trying to extend the behavior of the #NotBlank constraint to apply to URIs by making a custom constraint called #NotBlankUri.
Here's my constraint annotation:
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
public #interface NotBlankUri {
String message() default "{project.model.NotBlankUri.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and here is the ConstraintValidator:
public class NotBlankUriValidator implements ConstraintValidator<NotBlankUri, URI> {
public void initialize(NotBlankUri annotation) {
}
public boolean isValid(URI uri, ConstraintValidatorContext context) {
NotBlankValidator nbv = new NotBlankValidator();
return nbv.isValid(uri.toString(), context);
}
}
Problem is that the isValid() method on the ConstraintValidator is getting null values for the URI argument. I thought this wasn't supposed to happen given the fact that #NotBlank itself is annotated #NotNull. That not being the case, I tried adding #NotNull as a meta-annotation to my #NotBlankUri, but that didn't have the desired effect either. How can I make my annotation constraint behave like #NotBlank, which seems to be stacking on top of the behavior of #NotNull?
As per the documentation, you can't use the #NotBlank annotation on a datatype that is not a String.
public #interface NotBlank
Validate that the annotated string is not null or empty. The difference to NotEmpty is that trailing whitespaces are getting ignored.
So if you declared your validator to validate a String, everything would be fine and you could write your annotation like this:
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
#NotBlank
public #interface NotBlankUri {
String message() default "{project.model.NotBlankUri.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
If you are deadset on using the URI class 1 you need to perform custom validation logic yourself like this:
Annotation:
#NotNull(message="URI must not be null")
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
public #interface NotBlankUri {
String message() default "URI must not be blank";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class NotBlankUriValidator implements ConstraintValidator<NotBlankUri, URI> {
public void initialize(NotBlankUri annotation) {
}
public boolean isValid(URI uri, ConstraintValidatorContext context) {
boolean isValid = true;
System.out.println("URI: " + uri);
//Leave null checks to your #NotNull constraint.
//This is only here to prevent a NullPointerException on the next check.
if(uri == null){
return true;
}
if(uri.toString().isEmpty()){
isValid = false;
}
return isValid;
}
}
I ran the above with a test harness:
public class UriContainer {
public UriContainer(URI uri){
this.uri = uri;
}
#NotBlankUri
private URI uri;
public URI getUri() {
return uri;
}
}
public static void main(String[] args) throws URISyntaxException{
UriContainer filledContainer = new UriContainer(new URI("Stuff"));
UriContainer emptyContainer = new UriContainer(new URI(""));
UriContainer nullContainer = new UriContainer(null);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<UriContainer>> filledViolations = validator
.validate(filledContainer);
Set<ConstraintViolation<UriContainer>> emptyViolations = validator
.validate(emptyContainer);
Set<ConstraintViolation<UriContainer>> nullViolations = validator
.validate(nullContainer);
System.out.println("Filled: ");
filledViolations.stream().forEach(System.out::println);
System.out.println("Empty: ");
emptyViolations.stream().forEach(System.out::println);
System.out.println("Null: ");
nullViolations.stream().forEach(System.out::println);
}
which output the following violations:
URI: Stuff
URI:
URI: null
Filled:
Empty:
ConstraintViolationImpl{interpolatedMessage='URI must not be blank', propertyPath=uri, rootBeanClass=class sandbox.UriContainer, messageTemplate='URI must not be blank'}
Null:
ConstraintViolationImpl{interpolatedMessage='URI must not be null', propertyPath=uri, rootBeanClass=class sandbox.UriContainer, messageTemplate='URI must not be null'}
As you can see, this allows you to output different error messages based on if the URI is blank or null. Just make sure if you are using a javax.validation annotation you check which datatype you operate on.
1: which by the way, performs validation when you construct the object, and will throw a URISyntaxException if the String passed to the constructor violates RFC 2396