I have the next annotation with #PropertyMapping:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#ImportAutoConfiguration
#PropertyMapping("spring.test.testcontainers.datasource")
public #interface AutoConfigureDatasourceContainer {
#PropertyMapping("containers")
DatasourceContainer[] containers() default {};
}
And child annotation:
#Target({})
#Retention(RUNTIME)
public #interface DatasourceContainer {
#PropertyMapping("port")
int port() default 31335;
#PropertyMapping("username")
String username() default "";
#PropertyMapping("password")
String password() default "";
#PropertyMapping("database")
String database() default "";
#PropertyMapping("type")
DatasourceType type() default DatasourceType.MYSQL;
}
I expect that usages of this two annotations in next form:
#AutoConfigureDatasourceContainer(containers =
{
#DatasourceContainer(username = "username", password = "password", database = "users", type = DatasourceType.MYSQL)
})
will produce next property:
spring.test.testcontainers.datasource.containers[0].port=31335
spring.test.testcontainers.datasource.containers[0].username=username
.....
and so on. But this is not so.
I didn't find any examples in documentation about situations like this.
What is wrong with my code?
Related
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"}
I'm creating a custom ConstraintValidator to validate that my JodaTime object's hours are within a certain window when entered from a spring form.
My annotation:
#Target({ElementType.METHOD, ElementType.FIELD})
#Documented
#Constraint(validatedBy = InputHoursValidator.class)
#Retention(RetentionPolicy.RUNTIME)
public #interface InputHoursConstraint {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
My Validator
public class InputHoursValidator implements ConstraintValidator<InputHoursConstraint, DateTime> {
private static final DateTimeFormatter HOURS_TIME_FORMAT = DateTimeFormat.forPattern("hh:mma");
private static final String EARLIEST_START_TIME = "5:00pm";
private static final String LATEST_END_TIME = "4:00am";
#Override
public void initialize(InputHoursConstraint constraintAnnotation) {
}
#Override
public boolean isValid(DateTime value, ConstraintValidatorContext context) {
return !value.isBefore(DateTime.parse(EARLIEST_START_TIME, HOURS_TIME_FORMAT))
&& !value.isAfter(DateTime.parse(LATEST_END_TIME, HOURS_TIME_FORMAT).plusDays(1));
}
}
And my mojo with the annotation
public class HoursTrackingForm {
#NotNull(message = "Please enter a valid time in AM or PM")
#DateTimeFormat(pattern = "hh:mma")
#InputHoursConstraint(message = "Start time was before 5:00pm or after 4:00am")
private DateTime startTime;
#NotNull(message = "Please enter a valid time in AM or PM")
#DateTimeFormat(pattern = "hh:mma")
#InputHoursConstraint(message = "End time was before 5:00pm or after 4:00am")
private DateTime endTime;
//getters and setters
}
It all looks fine to me but when I submit my object for validation, DateTime in the validator is always null.
My issues were two fold.
1) if I was testing a null scenario I didn't realize all of the constraints still get validated even if one finds an error. Therefore while not null caused an validation error my custom constraint would still throw an NPE. Solution for that was to remove the #NotNull and do that check as well in #InputHoursConstraint.
2) In my validation annotation I added ElementType.TYPE and ElementType.LOCAL_VARIABLE in #Target which appears to allow it to work. Still doing research into why because based on my understanding I only needed ElementType.FIELD
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
The annotation class Get.java
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#interface Get {
String value();
}
The annotation class Field.java
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#interface Field {
String value();
}
The interface class GetInterface.java
public interface GetInterface {
#Get("/group/user?")
void getUser(#Field("id") String id);
}
I want to use this GetInterface in the following, how can I get the annotation name so I can use that as the parameter attribute to construct a get url with query parameters
public class Request implements GetInterface {
#Override
public getUser(String id) {
String getPath = "/group/user?"; //How can I get this path from the annotation?
String name = "how can I get the name attribute 'id'????";
String value = id; //this will be 123 in this example
//with the name an value, I can then construct a url like this
//https://www.foo.com/group/user?id=123
}
public static void main(String[] args) {
getUser("123);
}
}
Do the following:
Method m = GetInterface.class.getMethod("getUser");
Get getAnnotation = m.getAnnotation(Get.class)
Field fieldAnnotation = m.getParameters()[0].getAnnotation(Field.class)
String path = getAnnotation.value();
String fieldName = fieldAnnotation.value();
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