How insert field name when using internationalization with javax validations? - java

I'm using javax validation annotations with Spring Boot and internationalization. So I have the following field:
#Size(min = 3, max = 3, message = "{javax.validation.constraints.Size.message}")
private String currencyCode;
The US resource bundle has:
javax.validation.constraints.Size.message = size must be between {min} and {max}
So when this field fails on validation, I see the message "size must be between 3 and 3".
But what I want is to have a message like:
javax.validation.constraints.Size.message = {fieldName}'s size must be between {min} and {max}
Which would result in the message "currencyCode's size must be between 3 and 3".
Is this possible? Do I need to override a bean to make it work? Is there a pre-defined property for the field name?

You can create handler for MethodArgumentNotValidException
#ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { ...
inside them you can invoke
e.getBindingResult().getFieldErrors();
This is collection with all informations about invalid field error eg. field name, rejected value.
At next, use message like in your's example and use StrSubstitutor from org.apache.commons.lang3. Just create map of parameters
key --> fieldName , value --> currencyCode
Of course you have to write some code, but here is the solution :)

By default #Size annotation does not provide option to put label in the message, instead you can create your own implementation like below so that it can be used across your application:
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = MySizeValidator.class)
public #interface MySize {
String message() default "{javax.validation.constraints.Size.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default Integer.MAX_VALUE;
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
#Retention(RUNTIME)
#Documented
#interface List {
Size[] value();
}
public abstract String label();
}
then create your own validator like MySizeValidator to validate MySize annotation like below
public class MySizeValidator implements ConstraintValidator<MySize, String> {
private static final Log log = LoggerFactory.make();
private int min;
private int max;
#Override
public void initialize(MySize parameters) {
min = parameters.min();
max = parameters.max();
validateParameters();
}
#Override
public boolean isValid(String field, ConstraintValidatorContext constraintValidatorContext) {
if (field == null) {
return true;
}
int length = field.length();
return length >= min && length <= max;
}
private void validateParameters() {
if (min < 0) {
throw log.getMinCannotBeNegativeException();
}
if (max < 0) {
throw log.getMaxCannotBeNegativeException();
}
if (max < min) {
throw log.getLengthCannotBeNegativeException();
}
}
}
Create ValidationMessages.properties with below key value
javax.validation.constraints.mySize.message={label}'s size must be between {min} and {max}
then in your POJO, you can put this new annotation,
#MySize(min = 3, max = 3, message = "{javax.validation.constraints.mySize.message}", label = "Currency Code")
private String currencyCode;

Related

Can we add parameters to Json validation annonations?

I am doing the json field validation as below :
#Valid
#NotEmpty(message="Id must not be empty")
#Size(min=1, max=70, message="Id accepts max length of 70")
private String Id;
#Valid
#NotEmpty(message="application must not be empty")
#Size(min=1, max=8, message="application accepts max length of 8")
private String application;
This is working fine
but now i have got another requirement where while validating the second field i need to pass the id as well to the message, stating this application message is coming for which id
Example : application must not be empty for id = 123
or
application accepts max length of 8 for id = 456
I am not sure if i can pass other attribute from model to the message
If you have any clue please comment
To my knowledge, it is only possible to get current validated value, and the values in the annotation methods(min, max, etc). If someone actually knows of a way, please feel free to correct me.
To achieve access to another property value, you would need custom validation annotation on the type level. I'll assume the class holding data is called MyModel. Annotation might look like this:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MyModelApplicationSizeValidator.class)
public #interface MyModelApplicationSize {
String message() default "default message";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int min() default 0;
int max() default Integer.MAX_VALUE;
}
And the actual validator for this annotation:
import com.example.random.model.MyModel;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class MyModelApplicationSizeValidator implements ConstraintValidator<MyModelApplicationSize, MyModel> {
private int min;
private int max;
private String msgFormat;
#Override
public void initialize(MyModelApplicationSize constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
this.msgFormat = constraintAnnotation.message();
}
#Override
public boolean isValid(MyModel model, ConstraintValidatorContext context) {
String value = model.getApplication();
if (value == null || value.isEmpty() || value.length() < this.min || value.length() > this.max) {
context.disableDefaultConstraintViolation();
String message = String.format(this.msgFormat, this.min, this.max, value, model.getId());
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
return false;
}
return true;
}
}
Keep in mind building error message with String.format is not the best way to do it, it's kind of brittle, you might actually need to implement it in another way, depending on your needs.
And MyModel, validated with the new annotation:
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
#MyModelApplicationSize(message = "application accepts min length of %d and max length of %d for val - %s with id - %s", min = 2, max = 7)
public class MyModel {
#NotEmpty(message="Id must not be empty")
#Size(min=1, max=70, message="Id accepts max length of 70")
private String id;
#NotEmpty(message="application must not be empty")
#Size(min=1, max=8, message="application accepts min length of {min} and max length of {max} for val - ${validatedValue}")
private String application;
//getters and setters
}

ConstraintValidation Not Working for a Rest Api Request

I created an API and added an custom-annotation to validate the Request body object, but this was never getting called. Below is the Object. Please go through the code and help me out where the code need to be corrected?
#NotNull, #Size is also not working
Request Body Object
#Getter
#AllArgsConstructor
#Sample
public class SaleRequest {
#NotNull
private Integer sale;
#NotNull
private Date dateTime;
#NotNull
#Size(min = 10, max = 10)
private String customerId;
}
Annotation
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {SalesRequestValidator.class})
#Documented
public #interface Sample {
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String message() default "Invalid Sale Request";
}
Validator
public class SalesRequestValidator implements ConstraintValidator<Sample, SaleRequest> {
private String message;
#Override
public void initialize(Sample constraintAnnotation) {
this.message = constraintAnnotation.message();
}
#Override
public boolean isValid(SaleRequest sale, ConstraintValidatorContext context) {
System.out.println("Tested!");
return sale.getSale() > 0;
}
}
Api Implementation
public interface SalesApi {
#RequestMapping(
value = {"/sales"},
produces = {"application/json"},
consumes = {"application/json"},
method = {RequestMethod.POST}
)
ResponseEntity<Integer> submitSale(#RequestBody #Valid SaleRequest saleRequest);
}
Could not figure where I went wrong
Implementation looks ok.
Make sure you have set the #Valid annotation in your controller method where you expect to receive the request body of SaleRequest.
It should look something like this:
addNewSaleRequest(#RequestBody #Valid SaleRequest saleRequest)
Try to extend #Target({ElementType.TYPE}) with ElementType.PARAMETER as you want to validate a method parameter with it.
A #Validated annotation is needed on the related Controller as well.

javax validation greater or less than from other property

with respect to javax.validation
#NotNull(message = "From can't be null")
#Min(value = 1, message = "From must be greater than zero")
private Long from;
#NotNull(message = "To can't be null")
#Min(value = 1, message = "To must be greater than zero")
private Long to;
I want to also validate that FROM should be less than TO and TO should be greater than FROM ? how we can do this using javax validation's annotation ?
You need a custom cross field validation annotation.
One way is to annotate your custom class with #YourCustomAnnotation.
In YourCustomAnnotationValidator you have access to your value, hence you can implement your logic there:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Constraint(validatedBy = DateValidator.class)
public #interface RangeCheck {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class RangeCheckValidtor implements ConstraintValidator<RangeCheck, YourDto> {
#Override
public void initialize(RangeCheck date) {
// Nothing here
}
#Override
public boolean isValid(YourDto dto, ConstraintValidatorContext constraintValidatorContext) {
if (dto.getFrom() == null || dto.getTo() == null) {
return true;
}
return from < to;
}
}
Then mark your YourDto class with #RangeCheck:
#RangeCheck(message = "your messgae")
public class YourDto {
// from
// to
}
Or simply manually validate the relation of two fields.

How to access a field which is described in annotation property

Is it possible to access a field value, where field name is described in annotation which annotate another field in class.
For example:
#Entity
public class User {
#NotBlank
private String password;
#Match(field = "password")
private String passwordConfirmation;
}
Annotation:
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface Match {
String message() default "{}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String field();
}
Now, is it possible to access field password from class User in ConstraintValidator implementaion class?
Edit:
I wrote something like this:
public class MatchValidator implements ConstraintValidator<Match, Object> {
private String mainField;
private String secondField;
private Class clazz;
#Override
public void initialize(final Match match) {
clazz = User.class;
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
}
}
secondField = match.field();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext constraintValidatorContext) {
try {
Object o; //Now how to get the User entity instance?
final Object firstObj = BeanUtils.getProperty(o, mainField);
final Object secondObj = BeanUtils.getProperty(o, secondField);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
} catch (final Exception ignore) {
ignore.printStackTrace();
}
return true;
}
}
Now the question is how can I get the User object instance and compare fields values?
#Hardy Thenks for tip. Finally wrote some code which matches (more or less) expected result.
I'll paste it here, maybe will help someone to solve his problem.
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Match {
String field();
String message() default "";
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MatchValidator.class)
#Documented
public #interface EnableMatchConstraint {
String message() default "Fields must match!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MatchValidator implements ConstraintValidator<EnableMatchConstraint, Object> {
#Override
public void initialize(final EnableMatchConstraint constraint) {}
#Override
public boolean isValid(final Object o, final ConstraintValidatorContext context) {
boolean result = true;
try {
String mainField, secondField, message;
Object firstObj, secondObj;
final Class<?> clazz = o.getClass();
final Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Match.class)) {
mainField = field.getName();
secondField = field.getAnnotation(Match.class).field();
message = field.getAnnotation(Match.class).message();
if (message == null || "".equals(message))
message = "Fields " + mainField + " and " + secondField + " must match!";
firstObj = BeanUtils.getProperty(o, mainField);
secondObj = BeanUtils.getProperty(o, secondField);
result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
if (!result) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
break;
}
}
}
} catch (final Exception e) {
// ignore
//e.printStackTrace();
}
return result;
}
}
And how to use it...? Like this:
#Entity
#EnableMatchConstraint
public class User {
#NotBlank
private String password;
#Match(field = "password")
private String passwordConfirmation;
}
You either need to write a class level constraint in which you get the full User instance passed into the isValid call or you can use something like #ScriptAssert.
At the moment it is not possible to access the root bean instance as part of a "normal" field validation. There is a BVAL issue - BVAL-237 - which discusses to add this functionality, but so far it is not yet part of the Bean Validation specification.
Note, there are good reasons why the root bean is not accessible atm. Constraints which rely on the root bean being accessible will fail for the validateValue case.
Hack around... Hack because these internal Hibernate implementations won't work when they migrate to Java modules.
#Override
public boolean isValid(final Serializable value, final ConstraintValidatorContext context) {
var context = (ConstraintValidatorContextImpl) context;
//no-inspection unchecked
var descriptor = (ConstraintDescriptorImpl<Exists>) context.getConstraintDescriptor();
var existsAnnotation = descriptor.getAnnotationDescriptor().getAnnotation();
// Removed b/c irrelevant
}

Java Annotation at runtime

the following class exists which consist from predefined UUID's that describe possible entires of the database.
public class Predefined {
#NotNull
#Size(min = 1, max = 25)
public UUID phone = UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0");
#NotNull
#Size(min = 1, max = 40)
public UUID company = UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2");
}
Those values are received as a key pair value trough web service: and then they are put to a hashmap.
47b58767-c0ad-43fe-8e87-c7dae489a4f0 = +00112233445566778899
f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2 = someVirtualCompnayName
When i receive an UUID that i know i am creating an instance of the Predefined class and then getting the annotations of the filed in the Predefined class i.e.:
Annotation[] annon = field.getDeclaredAnnotations();
Now I need to check those annotation agains the values that I got from the web services i.e. “+00112233445566778899” and “someVirtualCompnayName” at runtime
Is this possible?
I am especially interesting in example covering JSR 303.
Shortly why I have such construct:
The DAO , #Repository classes have different structure i.e.
contact
contact_attrbute
contact_attibute_type
where the databse “contact_attibute_type” is meant for “company” and “phone”. The second table i.e. “contact_attrbute” is meant for the actual values of “company” and “phone”.
Now I need a way to validate those values before I write them in hibernate, thus I am getting the “public UUID phone” and then trying to apply those constrains to the actual value I got from the user i.e. “+00112233445566778899”.
I'll post the complete code I have come up with to validate your test-case (including a simple executable demo):
Annotations:
package annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target( {ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface NotNull
{
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target( {ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface Size
{
int min() default 0;
int max();
}
The Predefined class:
public class Predefined
{
#NotNull
#Size(min = 1, max = 25)
public UUID phone;
#NotNull
#Size(min = 1, max = 40)
public UUID company;
public Predefined(UUID phone, UUID company)
{
this.phone = phone;
this.company = company;
}
}
The validator class which iterates through the declared fields and checks their annotation and field/value mappings:
public class PredefinedValidator
{
public boolean validate(Predefined predefined, Map<UUID, String> mappings)
{
if (predefined == null)
return false;
for (Field field :predefined.getClass().getDeclaredFields())
{
if (field.getType().equals(UUID.class))
{
try
{
Annotation[] annotations = field.getDeclaredAnnotations();
UUID uuid = (UUID)field.get(predefined);
if (!this.validateField(uuid, annotations, mappings))
return false;
}
catch (IllegalArgumentException | IllegalAccessException ex)
{
Logger.getLogger(PredefinedValidator.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return true;
}
private boolean validateField(UUID field, Annotation[] annotations, Map<UUID, String> mapping)
{
boolean containsSize = false;
boolean containsNotNull = false;
int minSize = -1;
int maxSize = -1;
// fetch which annotations are available for the provided field
for (Annotation annotation : annotations)
{
if (annotation instanceof Size)
{
containsSize = true;
Size size = (Size)annotation;
minSize = size.min();
maxSize = size.max();
}
else if (annotation instanceof NotNull)
containsNotNull = true;
}
// check if the provided value is null and an annotatition for #NotNull
// is set
if (field == null && containsNotNull)
return false;
if (containsSize)
{
// get the value of the mapped UUID which we are going to validate
String value = mapping.get(field);
if (value == null && containsNotNull)
return false;
else if (value == null)
return true;
// check if the length of the value matches
if (value.length() <= minSize || value.length() >= maxSize)
return false;
}
// passed all tests
return true;
}
}
Last but not least a simple demo:
public static void main(String ... args)
{
Map<UUID, String> mappings = new HashMap<>();
mappings.put(UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0"), "+00112233445566778899");
mappings.put(UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"), "someVirtualCompnayName");
Predefined predefined = new Predefined(
UUID.fromString("47b58767-c0ad-43fe-8e87-c7dae489a4f0"),
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
Predefined predefined2 = new Predefined(
UUID.randomUUID(),
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
Predefined predefined3 = new Predefined(
null,
UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"));
PredefinedValidator validator = new PredefinedValidator();
System.out.println("predefined is valid: "+validator.validate(predefined, mappings));
System.out.println("predefined is valid: "+validator.validate(predefined2, mappings));
System.out.println("predefined is valid: "+validator.validate(predefined3, mappings));
mappings.put(UUID.fromString("f9a1e8f4-b8c0-41f2-a626-49c11da8d5c2"), "someVirtualCompnayNamesomeVirtualCompnayNamesomeVirtualCompnayNamesomeVirtualCompnayName");
System.out.println("predefined is valid: "+validator.validate(predefined, mappings));
}
HTH

Categories