I want to limit the user of Spring MVC to access only certain values of the enum, I need to throw constraint exception when the requested param contains the restricted value.
Enum Example:
public enum EnumActionValues {
WAIT,
OFFLINE,
LOGGED_IN,
LOGGED_OUT,
OTHERS,
//
;
public List<EnumActionValues> getManuallyAllowedActions() {
return Arrays.asList(
WAIT,
OFFLINE,
OTHERS
);
}
}
In the above enum I want to webrequest to the Controller should contain only getManuallyAllowedActions, the LOGGED_IN and LOGGED_OUT shouldn't be allowed by user, which will be used internally.
Is there any direct annotations to be used with #Valid/#Validated.
You can have a custom annotation and a validator that goes with it.
Your annotation could look like this:
#Documented
#Constraint(validatedBy = YourConstraintValidator.class)
#Target( { ElementType.FIELD } )
#Retention(RetentionPolicy.RUNTIME)
public #interface YourConstraint
{
String message() default "Invalid enum";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And your validator would be:
public class YourConstraintValidator implements ConstraintValidator<YourConstraint, EnumActionValues> {
#Override
public void initialize(YourConstraint constraint) {
}
#Override
public boolean isValid(EnumActionValues obj, ConstraintValidatorContext context) {
return obj == null || obj.getManuallyAllowedActions().contains(obj);
}
}
This validator allows for the enum to be null so it will still work in case the enum is null in the request.
Note that you will have to use #ModelAttribute annotation instead of #RequestParam for this to work.
I think your requirement here is very specific and you probably have to write the check yourself. Something like this should do the trick:
public ResponseEntity someEndpoint(final EnumActionValues aAction) {
if ((aAction != null) && !EnumActionValues.getManuallyAllowedActions().contains(aAction)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
...
}
Related
I have a variable called notification.max-time-to-live in application.yaml file and want to use this as the value of javax.validation.constraints.#Max() annotation.
I've tried in many ways (using env.getProperty(), #Value, etc) and it says it must be a constant value, is there any way to do this?
I know this does not directly answer my question and as M. Deinum already said the answer is no. Nonetheless it's a simple workaround.
It's true that #Max and other javax annotations do not let us use dynamic values, however, we can create a custom annotation (as M. Deinum suggested) that uses values from application.yaml with spring #Value.
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = ValidTimeToLiveValidator.class)
public #interface ValidTimeToLive {
String message() default "must be less than or equal to %s";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
And the respective validator.
public class ValidTimeToLiveValidator implements ConstraintValidator<ValidTimeToLive, Integer> {
#Value("${notification.max-time-to-live}")
private int maxTimeToLive;
#Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
// leave null-checking to #NotNull
if (value == null) {
return true;
}
formatMessage(context);
return value <= maxTimeToLive;
}
private void formatMessage(ConstraintValidatorContext context) {
String msg = context.getDefaultConstraintMessageTemplate();
String formattedMsg = String.format(msg, this.maxTimeToLive);
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(formattedMsg)
.addConstraintViolation();
}
}
Now we just need to add this custom annotation in the respective class.
public class Notification {
private String id;
#ValidTimeToLive
private Integer timeToLive;
// ...
}
A custom enum validator annotation interface:
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = PanTypeSubSetValidator.class)
public #interface PanTypeSubset {
PanType[] anyOf();
String message() default "must be any of {anyOf}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and the actual implementation:
public class PanTypeSubSetValidator implements ConstraintValidator<PanTypeSubset, PanType> {
private PanType[] subset;
#Override
public void initialize(PanTypeSubset constraint) {
this.subset = constraint.anyOf();
}
#Override
public boolean isValid(PanType value, ConstraintValidatorContext context) {
return value == null || Arrays.asList(subset).contains(value);
}
}
and the usage inside a request DTO:
#SuperBuilder
#Data
#NoArgsConstructor
public class PanBaseRequestDto {
#NotNull(message = "'PANTYPE' cannot be empty or null")
#PanTypeSubset(anyOf = {PanType.PAN, PanType.TOKEN}, message = "yesssss")
private PanType panType;
}
The problem is that this annotation never seems to be triggered. I get another exception kick in in the #RestControllerAdvice DefaultExceptionHandler implementation before this actual validation:
Handling generic exception: (Invalid JSON input: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]
Solved it by creating a custom #JsonCreator function inside the ENUM class. Not the best approach, as we loose the value that user has submitted when displaying error to the end client, but it's ok for me.
#JsonCreator
public static PanType create(String value) {
if (Objects.isNull(value)) {
return null;
}
return Arrays.stream(PanType.values())
.filter(v -> value.equals(v.getType()))
.findFirst()
.orElse(PanType.UNKNOWN);
}
I receive list of domain object ids deserialized from JSON client request body:
#JsonProperty("workgroups")
private List<WorkgroupId> workgroupIds = new ArrayList<>();
I need to validate these ids in org.springframework.validation.Validator.
for (WorkgroupId workgroupId : project.getWorkgroupIds()) {
if (!domainObjectTools.doesWorkgroupExist(workgroupId)) {
// reject this invalid value here...
}
}
Question
How to reject invalid value in org.springframework.validation.Errors?
Validator interface:
#Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = { CustomValidatorValidator.class })
#Documented
public #interface CustomValidator{
String message() default "Put here your default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValidatorImplementation:
public final class CustomValidatorValidator implements ConstraintValidator<CustomValidator, List<WorkgroupId>> {
#Override
public void initialize(CustomValidator constraintAnnotation) {
}
#Override
public boolean isValid(List<WorkgroupId> yourlist, ConstraintValidatorContext context) {
if (yourlist== null)
return true;
else
return yourlist.stream().anyMatch(s -> /* filter here as you want */);
}
}
Notice we return true if the field is null, I do it like this because I just put #NotNull constraint if I need not to be null so I have more control over the constraint.
Finally:
#JsonProperty("workgroups")
#CustomValidator
private List<WorkgroupId> workgroupIds = new ArrayList<>();
P.S: I don't understand why you initialize the list in this last code. If that's a field you're supposed to receive through the request then you don't need to initialize it, the json deserializer will initialize it with the incoming field in the json.
You can use reject() and/or rejectValue() along with the field/error code/defaultMessage or along with a custom validator to reject the values.
I'm trying to implement a custom annotation to validate my fields. The idea is that the validation fails whenever the annotated field is null. Something like this.
#RequiredProperty
public abstract Person getPerson();
Now if this returns a null Person, I'd like the validation to fail (ideally with a custom message "Person field is null").
I tried to do it like this.
#Documented
#Constraint(validatedBy = RequiredPropertyValidator.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
#ReportAsSingleViolation
public #interface RequiredProperty {
String message() default "{javax.validation.constraints.RequiredProperty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And validator.
public class RequiredPropertyValidator implements ConstraintValidator<RequiredProperty, Object> {
#Override
public void initialize(RequiredProperty constraintAnnotation) {
}
#Override
public boolean isValid(Object property, ConstraintValidatorContext context) {
return property != null;
}
}
However this won't work. It doesn't validate at all. Object property is never even passed to the isValid method. Any advice on how to get it working?
UPDATE
Removing the empty initialize method got it working. However, I'm not sure how to create a custom error message that the "Person" field is null. Is that possible?
I created a custom message in .properties file, but this is just a static message, and I'd like to capture the actual field from runtime.
Like that
#NotNull(code=10000)
#Size(min=5, max=10, code=10001)
Java bean validation has 3 properties: message, payload and groups. I wanna add a new one: code.
I've checked some docs, like https://docs.jboss.org/hibernate/validator/5.0/reference/en-US/html/validator-customconstraints.html and https://docs.oracle.com/javaee/6/tutorial/doc/gkfgx.html. It seems not possible?
Short answer to your question: No, you can't just add an extra field, nor can you use inheritance too add the extra field. See this question which explains why Java doesn't allow inheritance with annotations.
What you'll have to do is create your own particular version of the #Size annotation. But you should be able to apply the #Size annotation to your custom annotation so that #Size is checked automatically when you run your validations.
Your annotation would probably look something like this:
#Constraint(validatedBy = { })
#Target({ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#ReportAsSingleViolation
//The only downside of this approach
//is that you have to hardcode your min and max values
#Size(min=5, max=10)
public #interface CodedSize {
String message() default "{Default Validation Message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int code() default 0;
}
If you wanted to specify the size as well in the annotation, you could do that, and write a custom validator that validates your annotation.
#Constraint(validatedBy = { CodedSizeValidator.class })
#Target({ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface CodedSize {
String message() default "{Default Validation Message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int minSize() default 5;
int maxSize() default 10;
int code() default 0;
}
Then your custom validator looks something like this:
public class CodedSizeValidator implements ConstraintValidator<CodedSize, String> {
private int minSize;
private int maxSize;
private int code;
#Override
public void initialize(CodedSize constraintAnnotation){
this.minSize = constraintAnnotation.minSize();
this.maxSize = constraintAnnotation.maxSize();
this.code = constraintAnnotation.code();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean isValid = false;
if(value == null || value.isEmpty()) {
//if a null or empty value is valid, then set it here.
isValid = true;
} else {
//Logic here to determine if your value is valid by size constraints.
}
return isValid;
}
}
I used String because that seemed the most applicable, but you could easily use Number, or even a generic type to allow you to use this annotation on more than one field. The advantage of doing it this way is that if you want to add a null check in with this validation, you can do so.
The ConstraintValidatorContext can be used to build out your error message if multiple validations fail. You can make this as detailed as you want, but be aware that this code can spaghetti-fy very quickly.