Build own new message from BindingResult - spring

I would like to consult you about the BindingResult. Take a look at my situation below.
My messages.properties store error message as follows
error.required=Please input {0}
My controller has rejected 2 fields with the same error as follows
bidingResult.rejectValue("field1", "error.required", new Object[]{"Field1"}, null);
bidingResult.rejectValue("field2", "error.required", new Object[]{"Filed2"}, null);
My view has extract messages by used Thymeleaf syntax as follows
<div th:each="err : ${#fields.errors('*')}" th:text="${err}"></div>
The result will show 2 messages as follows
Please input Field1
Please input Field2
My question is can I create a new messages object by BindingResult to show a message like this
Please input Field1 and Field2
I want to merge the same error together and concatenate message param

The easiest way to do that will be to create a custom validator at the object level. Start with a custom annotation:
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 = BothFieldsPresentValidator.class)
public #interface BothFieldsPresent {
String message() default "{BothFieldsPresent}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Next, create the custom validator:
public class BothFieldsPresentValidator implements ConstraintValidator<BothFieldsPresent, MyObject> {
public boolean isValid(MyObject myObject, ConstraintValidatorContext context) {
if( myObject.getField1() == null && myObject.getField2() == null ) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("{BothFieldsPresent}")
.addConstraintViolation()
return false;
}
return true;
}
}
Add the new custom annotation to your MyObject class:
#BothFieldsPresent
public class MyObject {
...
}
In your controller, do something like this:
#PostMapping
public String doPost(#Valid #ModelAttribute("myObject") MyObject myObject, BindingResult bindingResult, Model model) {
if( bindingResult.hasErrors() ) {
return "template-name"
}
service.doSomething(myObject);
return "redirect:/somewhere"
}
Add this to messages.properties:
BothFieldsPresent=Please input Field1 and Field2

Related

Bean validator: validate nested object while adding a prefix to its error messages

I'm having a problem where, when I have multiple nested beans of the same type, the returned message may end up being the same, which can confuse the user:
Minimal example (the real beans have lots of fields):
class A {
#NotBlank("Name is obligatory.")
String name;
#NotBlank("Address is obligatory.")
String name;
}
class B {
#Valid
A origin;
#Valid
A destination;
}
If I run B against the validator with blank names, it will always return "Name is obligatory.", no matter if it comes from the origin or from the destination. I know that the error message comes with the field names, but that information, by itself, is not very useful for the end user.
Is there some annotation that validates the nested beans similarly to what #Valid does, but adding a prefix, so that instead of saying "Name is obligatory.", it would say either "Original person: Name is obligatory." or "Destination person: Name is obligatory."?
I couldn't find any out-of-the box way to implement what was needed, so I had to implement a custom constraint:
PrefixConstraint.java
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Documented
#Constraint(validatedBy = PrefixConstraintValidator.class)
#Target( { ElementType.METHOD, ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface PrefixConstraint {
String message() default "Prefix missing";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PrefixConstraintValidator.java
import java.util.Set;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
public class PrefixConstraintValidator implements ConstraintValidator<PrefixConstraint, Object> {
private PrefixConstraint constraint;
#Override
public void initialize(PrefixConstraint constraintAnnotation) {
this.constraint = constraintAnnotation;
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(value, this.constraint.groups());
context.disableDefaultConstraintViolation();
for (ConstraintViolation<Object> violation : violations) {
context
.buildConstraintViolationWithTemplate(this.constraint.message() + ": " + violation.getMessage())
.addPropertyNode(violation.getPropertyPath().toString())
.addConstraintViolation();
}
return violations.isEmpty();
}
}
Usage example:
class A {
#NotBlank("Name is obligatory.")
String name;
#NotBlank("Address is obligatory.")
String name;
}
class B {
#PrefixConstraint(message = "Original person")
A origin;
#PrefixConstraint(message = "Destination person")
A destination;
}
Now, if the names are left blank, the validator will return the message: "Original person: Name is obligatory" and "Destination person: Name is obligatory".

Spring custom validator with dependencies on other fields

We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:
https://www.baeldung.com/spring-mvc-custom-validator
The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:
public class FundTransferRequest {
private String accountTo;
private String accountFrom;
private String amount;
#CustomValidator
private String reason;
private Metadata metadata;
}
public class Metadata {
private String channel; //e.g. mobile, web, etc.
}
Basically #CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.
Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.
Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.
The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation
In your case it might look something like this:
#Constraint(validatedBy = CustomValidator.class)
#Target({ ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomValidation {
String message() default "Reason required";
String checkedField() default "metadata.channel";
String checkedValue() default "WEB";
String requiredField() default "reason";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.demo;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/*
If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
Else, it will be required.
*/
#Component
public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
private String checkedField;
private String checkedValue;
private String requiredField;
#Override
public void initialize(CustomValidation constraintAnnotation) {
this.checkedField = constraintAnnotation.checkedField();
this.checkedValue = constraintAnnotation.checkedValue();
this.requiredField = constraintAnnotation.requiredField();
}
#Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
Object checkedFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(checkedField);
Object requiredFieldValue = new BeanWrapperImpl(value)
.getPropertyValue(requiredField);
return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
}
}
And the usage will be:
#CustomValidation
public class FundTransferRequest {
...
or with parameters specified:
#CustomValidation(checkedField = "metadata.channel",
checkedValue = "WEB",
requiredField = "reason",
message = "Reason required")
public class FundTransferRequest {
...

Validate image if existing or not null only in spring boot?

I'm trying to validate image in Spring boot with a custom messages and custom validator.
Here is my file path for the validator files
I just need to know how to check first if the image file is existing or not null and then validate it.
I need to mention that my image can be null value, in this case, I should not do the validation.
Here is the example for more clarification:
I first created the annotation as follows:
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.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {ImageFileValidator.class})
public #interface ValidImage {
String message() default "Invalid image file";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
I've created the validator as following:
import org.springframework.web.multipart.MultipartFile;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ImageFileValidator implements ConstraintValidator<ValidImage, MultipartFile> {
#Override
public void initialize(ValidImage constraintAnnotation) {
}
#Override
public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {
boolean result = true;
String contentType = multipartFile.getContentType();
if (!isSupportedContentType(contentType)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Only PNG or JPG images are allowed.")
.addConstraintViolation();
result = false;
}
return result;
}
private boolean isSupportedContentType(String contentType) {
return contentType.equals("image/png")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg");
}
}
Finally, applied the annotation as following:
public class CreateUserParameters {
#ValidImage
private MultipartFile image;
...
}
The code of the custom validator was here :
File upload in Spring Boot: Uploading, validation, and exception handling
by Wim Deblauwe in the last comment.
This is not really the answer for my question, but a temporary solution if someone has the same issue.
So the thing is, If the image is null, I will just tell the ImageFileValidator to send True to ValidImage and then the image will be validated.
Which means I don't have to send errors.
#Override
public boolean isValid(MultipartFile multipartFile, ConstraintValidatorContext context) {
boolean result = true;
String contentType = "";
try {
contentType = multipartFile.getContentType();
if(fileNameLength(multipartFile) == 0) {
return result;
}
} catch(NullPointerException e) {
e.printStackTrace();
}
if (!isSupportedContentType(contentType)) {
.....
The rest of the code can be found above. It might return NullException as well, so make sure to handle it.

Is there any way that we can use two custom error messages using spring-boot custom validation?

I'm using the below custom validation code to validate personName and it seems to be working fine, but the problem is when am passing an EMPTY string, it is giving same error message instead of the empty error message. Can someone please help me with this?
#Documented
#Constraint(validatedBy = {PersonNameValidator.class})
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#ReportAsSingleViolation
public #interface PersonName {
/**
* Default error message defined for the validator.
*
* #return message
*/
String message() default "invalid person name";
/**
* Method to define groups parameters for validation.
*
* #return groups
*/
Class[] groups() default {};
/**
* Method to load payload.
*
* #return payload
*/
Class<? extends Payload>[] payload() default {};
}
public class PersonNameValidator implements ConstraintValidator<PersonName, String> {
#Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (name.length() == 0) {
throw new IllegalArgumentException("must not be Empty");
} else if (!name.matches("(?=^(?!\\s*$).+)(^[^±!#£$%^&*_+§€#¢§¶•«\\\\/<>?:;|=]{1,256}$)")) {
throw new IllegalArgumentException("name should start with uppercase.");
}
return true;
}
}
#Data
public class NameDto {
#NotNull
#PersonName
private String family1Name;
#PersonName
private String family2Name;
#NotNull
#PersonName
private String givenName;
#PersonName
private String middleName;
}
Getting NullPointerException
#Name
#NotEmpty(message = "name cannot be empty")
String name;
should work
but if you want to join constraint you should use a custom ConstraintValidator add provide this validator via #Constraint(validatedBy = {YourCustomValidator.class}
see example below
full example
used dependency spring-boot-starter-validation (not needed if you use spring-boot-starter-web)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
application.properties
upper.name=dirk
application
package stackoverflow.demo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
#SpringBootApplication
public class SoCustomValidationApplication {
public static void main(String[] args) {
SpringApplication.run(SoCustomValidationApplication.class, args);
}
}
#Component
class ConfigurationLoader{
final MyCustomValidatedProperties config;
ConfigurationLoader(MyCustomValidatedProperties config){
this.config = config;
}
#EventListener()
void showName() {
System.err.println("name is: " + config.getName());
}
}
#org.springframework.context.annotation.Configuration
#Validated
#ConfigurationProperties(prefix = "upper")
class MyCustomValidatedProperties {
#Uppercase
#NotEmpty(message = "name cannot be empty")
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
#Constraint(validatedBy = {ValidNameValidator.class})
#Target({ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#ReportAsSingleViolation
#interface Uppercase {
String message() default "name should start with uppercase";
Class[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class ValidNameValidator implements ConstraintValidator<Uppercase, String> {
#Override
public boolean isValid(String name, ConstraintValidatorContext context) {
if (null == name || 0 == name.length() ) {
throw new IllegalArgumentException("name cannot be empty.");
} else if(!name.matches("^([A-Z][a-z]+)")) {
throw new IllegalArgumentException("name should start with uppercase.");
}
return true;
}
}
APPLICATION FAILED TO START
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'upper' to stackoverflow.demo.MyCustomValidatedProperties$$EnhancerBySpringCGLIB$$d0094cdb failed:
Property: upper.name
Value: dirk
Origin: class path resource [application.properties]:1:12
Reason: name should start with uppercase
and if you leave upper.name empty
upper.name=
APPLICATION FAILED TO START
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'upper' to stackoverflow.demo.MyCustomValidatedProperties$$EnhancerBySpringCGLIB$$29925f50 failed:
Property: upper.name
Value:
Origin: class path resource [application.properties]:1:12
Reason: name cannot be empty
Property: upper.name
Value:
Origin: class path resource [application.properties]:1:12
Reason: name should start with uppercase

Cross field validation with HibernateValidator displays no error messages

I'm validating two fields, "password" and "confirmPassword" on the form for equality using HibernateValidator as specified in this answer. The following is the constraint descriptor (validator interface).
package constraintdescriptor;
import constraintvalidator.FieldMatchValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface FieldMatch
{
String message() default "{constraintdescriptor.fieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* #return The first field
*/
String first();
/**
* #return The second field
*/
String second();
/**
* Defines several <code>#FieldMatch</code> annotations on the same element
*
* #see FieldMatch
*/
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
public #interface List{
FieldMatch[] value();
}
}
The following is the constraint validator (the implementing class).
package constraintvalidator;
import constraintdescriptor.FieldMatch;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.beanutils.BeanUtils;
public final class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
private String firstFieldName;
private String secondFieldName;
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
//System.out.println("firstFieldName = "+firstFieldName+" secondFieldName = "+secondFieldName);
}
public boolean isValid(final Object value, final ConstraintValidatorContext cvc) {
try {
final Object firstObj = BeanUtils.getProperty(value, firstFieldName );
final Object secondObj = BeanUtils.getProperty(value, secondFieldName );
//System.out.println("firstObj = "+firstObj+" secondObj = "+secondObj);
return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception e) {
System.out.println(e.toString());
}
return true;
}
}
The following is the validator bean which is mapped with the JSP page (as specified commandName="tempBean" with the <form:form></form:form> tag).
package validatorbeans;
import constraintdescriptor.FieldMatch;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
#FieldMatch.List({
#FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match", groups={TempBean.ValidationGroup.class})
})
public final class TempBean
{
#NotEmpty(groups={ValidationGroup.class}, message="Might not be left blank.")
private String password;
#NotEmpty(groups={ValidationGroup.class}, message="Might not be left blank.")
private String confirmPassword;
public interface ValidationGroup {};
//Getters and setters
}
UPDATE
It's all working correctly and does the validation intended. Just one thing remains is to display the specified error message above the TempBean class within #FieldMatch is not being displayed i.e only one question : how to display error messages on the JSP page when validation violation occurs?
(the annotation #NotEmpty on both of the fields password and confirmPassword in the TempBean class works and displays the specified messages on violation, the thing is not happening with #FieldMatch).
I'm using validation group based on this question as specified in this blog and it works well causing no interruption in displaying error messages (as it might seem to be).
On the JSP page these two fields are specified as follows.
<form:form id="mainForm" name="mainForm" method="post" action="Temp.htm" commandName="tempBean">
<form:password path="password"/>
<font style="color: red"><form:errors path="password"/></font><br/>
<form:password path="confirmPassword"/>
<font style="color: red"><form:errors path="confirmPassword"/></font><br/>
</form:form>
Could you try your isValid method to be like this? (this is certainly working for me in live project):
public boolean isValid(final Object value, final ConstraintValidatorContext cvc){
boolean toReturn = false;
try{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName );
final Object secondObj = BeanUtils.getProperty(value, secondFieldName );
//System.out.println("firstObj = "+firstObj+" secondObj = "+secondObj);
toReturn = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception e){
System.out.println(e.toString());
}
//If the validation failed
if(!toReturn) {
cvc.disableDefaultConstraintViolation();
//In the initialiaze method you get the errorMessage: constraintAnnotation.message();
cvc.buildConstraintViolationWithTemplate(errorMessage).addNode(firstFieldName).addConstraintViolation();
}
return toReturn;
}
Also I see that you are implementing the ConstraintValidator interface with an Object, literally. It should be the backing object that you have from your form:
tempBean // the one that you specify in the commandName actually.
So you implementation should like this:
implements ConstraintValidator<FieldMatch, TempBean>
This is probably not the issue here, but as a future reference, this is how it should be.
UPDATE
Inside your FieldMatch interface/annotation you have two methods : first and second, add one more called errorMessage for example:
Class<? extends Payload>[] payload() default {};
/**
* #return The first field
*/
String first();
/**
* #return The second field
*/
String second();
/**
#return the Error Message
*/
String errorMessage
Look inside you method from the Validation class - you are getting the first and second field names there., so just add the errorMessage, like this for example:
private String firstFieldName;
private String secondFieldName;
//get the error message name
private String errorMessagename;
public void initialize(final FieldMatch constraintAnnotation)
{
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
errorMessageNAme = constraintAnnotation.errorMessage();
//System.out.println("firstFieldName = "+firstFieldName+" secondFieldName = "+secondFieldName);
}
Inside isValida get it, the same way you do for first and second field name and use it.

Resources