Is there an annotation for java validate if the value of a field in a List of Objects is duplicated? - spring-boot

I have a List of Objects and each Object have an email, I'd like to validate if the email is duplicated in this list.
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
Father create(#PathVariable String id,
#Valid #RequestBody Father father) {
...
}
Father will have a list of child:
private List<Child> childs;
Each child will have an email:
public class Child {
...
#NotEmpty
private String email;
...
}
I'd like to validate if for example there is a request body with 2 child with the same email.
Is it possible or only validating after receive and process the payload?

Edited
For validating the child emails list, you can create a custom validation.
I coded a custom validation as follows
1- Create annotation named ChildEmailValidation
#Documented
#Constraint(validatedBy = ChildEmailValidator.class)
#Target( { ElementType.METHOD, ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface ChildEmailValidation {
String message() default "Duplicate Email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2- Create a validator for ChildEmailValidation
In this part, you can write your custom business for validation. (You can write your algorithm)
public class ChildEmailValidator implements ConstraintValidator<ChildEmailValidation, List<Child>> {
#Override
public void initialize(ChildEmailValidation constraintAnnotation) {
}
#Override
public boolean isValid(List<Child> childList, ConstraintValidatorContext constraintValidatorContext) {
//Create empty mailList
List<String> mailList = new ArrayList<>();
//Iterate on childList
childList.forEach(child -> {
//Checks if the mailList has the child's email
if (mailList.contains(child.getMail())) {
//Found Duplicate email
throw new DuplicateEmailException();
}
//Add the child's email to mailList (If duplicate email is not found)
mailList.add(child.getMail());
});
//There is no duplicate email
return true;
}
}
3- Add #ChildEmailValidation in Father class
public class Father {
List<Child> child;
#ChildEmailValidation
public List<Child> getChild() {
return child;
}
public void setChild(List<Child> child) {
this.child = child;
}
}
4- Put #Valid on fatherDto in the controller
#RestController
#RequestMapping(value = "/test/", method = RequestMethod.POST)
public class TestController {
#RequestMapping(value = "/test")
public GenericResponse getFamily(#RequestBody #Valid Father fatherDto) {
// ...
}
}

You can use #UniqueElements annotation of Hibernate that you can find on this documentation this is based on equals of some element.
#UniqueElements
private List<Child> childs;

Related

Issue in custom validation message using messages.properties file in spring boot validation

I am using spring validation to validate the Rest Controller input, I would appreciate if any one can tell me is there a possibility of throwing custom message in case of exception and the custom message should come from properties file.
UserController.java
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping(
value="/",
method=RequestMethod.POST,
consumes = MimeTypeUtils.APPLICATION_JSON_VALUE,
produces = MimeTypeUtils.APPLICATION_JSON_VALUE
)
public Object[] createUser(#ModelAttribute("user") User user, BindingResult bindingResult) {
new UserValidator().validate(user,bindingResult);
if (bindingResult.hasErrors()) {
return bindingResult.getFieldErrors().toArray();
}
}
UserValidator.java
public class UserValidator implements Validator{
#Override
public boolean supports(Class<?> aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "user.firstName.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "user.lastName.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "slug", "user.slug.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "user.email.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "user.password.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "phone", "user.phone.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "address", "user.address.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "country", "user.country.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "gender", "user.gender.empty");
User user = (User) obj;
Pattern pattern = Pattern.compile("^[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,6}$",
Pattern.CASE_INSENSITIVE);
if(!errors.hasErrors()) {
if (!(pattern.matcher(user.getEmail()).matches())) {
errors.rejectValue("email", "user.email.invalid");
}
}
}
}
messages.properties
# messages.properties
user.firstName.empty=Enter a valid first name.
user.lastName.empty = Enter a valid last name.
user.slug.empty = Select gender.
user.phone.empty = Select gender.
user.address.empty = Select gender.
user.country.empty = Select gender.
user.password.empty = Select gender.
user.gender.empty = Select gender.
user.email.empty = Enter a valid email.
user.email.invalid = Invalid email! Please enter valid email.
CustomMessageSourceConfiguration.java
#Configuration
public class CustomMessageSourceConfiguration {
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new
ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
#Bean
public LocalValidatorFactoryBean getValidator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
Browser Response
{codes: ["user.firstName.empty.user.firstName", "user.firstName.empty.firstName",…], arguments:
null,…}
codes: ["user.firstName.empty.user.firstName", "user.firstName.empty.firstName",…]
0: "user.firstName.empty.user.firstName"
1: "user.firstName.empty.firstName"
2: "user.firstName.empty.java.lang.String"
3: "user.firstName.empty"
arguments: null
defaultMessage: null
objectName: "user"
field: "firstName"
rejectedValue: null
bindingFailure: false
code: "user.firstName.empty"
Another Way Of Validation
We can add 4th Parameter As Error Message without using messages.properties file.
public class UserValidator implements Validator{
#Override
public boolean supports(Class<?> aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName",
"user.firstName.empty","Error Message Here");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName",
"user.lastName.empty","Error Message Here");
...
...
}
}
Another Way Of Validation
We can autowire MessageSource to UserController.java which is configured in CustomMessageSourceConfiguration.java to get messages.properties file.
#Autowired
private MessageSource messageSource;
public User createUser(#Valid #ModelAttribute("user") #RequestBody User
user) {
final ArrayList errorList = new ArrayList<>() {};
bindingResult.getFieldErrors().forEach(fieldError -> {
errorList.add(new
ObjectError(fieldError.getField(),messageSource.getMessage(fieldError.getCode(),
null, Locale.getDefault())));
});
Now we get required error message mapping from messages.properties file.
Way to Validate
We can create seperate bean validation.
Users.java
#Entity
#Table(name = "users")
public class User{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
#InBetweenNumberCustom(min = 12,max = 18)
private Integer age;
//getters and setters
}
Here we are going to create #InBetweenNumberCustom validation annotation.
import com.something.validator.ConstraintValidator.InBetweenNumberValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(InBetweenNumberCustom.List.class)
#Documented
#Constraint(validatedBy = {InBetweenNumberValidator.class})
public #interface InBetweenNumberCustom {
String message() default "Must be in between {min} and {max}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default 2147483647;
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface List {
InBetweenNumberCustom[] value();
}
}
InBetweenNumberValidator.java
import com.something.validator.annonations.InBetweenNumberCustom;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class InBetweenNumberValidator implements
ConstraintValidator<InBetweenNumberCustom,Integer> {
private int minValue;
private int maxValue;
#Override
public void initialize(InBetweenNumberCustom inBetweenNumberCustom) {
this.minValue = inBetweenNumberCustom.min();
this.maxValue = inBetweenNumberCustom.max();
}
#Override
public boolean isValid(Integer aInteger, ConstraintValidatorContext
constraintValidatorContext) {
// null values are not valid
if ( aInteger == null ) return false;
else return aInteger <= this.maxValue && aInteger >= this.minValue;
}
}
UserController.java
public JSONObject createUser(#Validated #RequestBody User user,
BindingResult bindingResult) {
...
...
}

Spring class level validation and Thymeleaf

I am learning Spring Framework and Thymeleaf. I have known how to display field error by using something like ${#fields.errors("xx")}. However, I get stuck about how to display object error message in Thymeleaf.
Here is my UserForm class:
#PasswordMatches
public class UserForm {
#NotNull
#NotEmpty
private String username;
#NotNull
#NotEmpty
private String password;
#NotNull
#NotEmpty
private String matchingPassword;
#NotNull
#NotEmpty
#ValidEmail
private String email;
/* setter and getter methods */
Here is my PasswordMatches annotation:
#Target({ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = PasswordMatchesValidator.class)
#Documented
public #interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
#Override
public void initialize(PasswordMatches constraintAnnotation) {
}
#Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getMatchingPassword());
}
}
Here is my Controller method:
#RequestMapping(value="/registration", method=RequestMethod.POST)
public ModelAndView registerUserAccount(#ModelAttribute("user") #Valid UserForm userForm,
BindingResult result, WebRequest request, Errors errors) {
if (!result.hasErrors()) {
return new ModelAndView("registerSuccess");
}
else {
return new ModelAndView("registration", "user", userForm);
}
}
Now here is my problem: If the password field and confirmPass field doesn't match, how can I get the default error message returned by the class level annotation in Thymeleaf?
I know this is old post but I also encountered this problem and here is the soulution (maybe it will also help someone else):
Modify PasswordMatchesValidator to this:
class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
#Override
public void initialize(PasswordMatches constraintAnnotation) {
}
#Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
boolean isValid = user.getPassword().equals(user.getMatchingPassword());
if(!isValid){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode( "matchingPassword" ).addConstraintViolation();
}
return isValid;
}
it will bind the validation result to your 'matchingPassword' attribute. So in your thymeleaf template us it like this:
${#fields.errors("matchingPassword")}
Add this inside the form tag:
<p data-th-each="err : ${#fields.allErrors()}" data-th-text="${err}" class="error">
Invalid input.
</p>
<p th:if="${#fields.hasErrors('${yourObject}')}" th:errors="${yourObject}"></p>

How do I validate a #QueryParam?

I've got a simple REST resource which accepts a couple of query parameters. I'd like to validate one of these parameters, and came across ConstraintValidator for this purpose. The REST resource expects the query param territoryId to be a UUID, so I'd like to validate that it indeed is a valid UUID.
I've created an #IsValidUUID annotation, and a corresponding IsValidUUIDValidator (which is a ConstraintValidator). With what I have now, nothing gets validated and getSuggestions accepts anything I throw at it. So clearly I'm doing something wrong.
What am I doing wrong?
The REST resource now looks like this :
#Component
#Path("/search")
public class SearchResource extends AbstractResource {
#GET
#Path("/suggestions")
#Produces(MediaType.APPLICATION_XML)
public Response getSuggestions(
#QueryParam("phrase") List<String> phrases,
#IsValidUUID #QueryParam("territoryId") String territoryId) {
[...]
}
}
IsValidUUID
#Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = {IsValidUUIDValidator.class})
public #interface IsValidUUID {
String message() default "Invalid UUID";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
IsValidUUIDValidator
public class IsValidUUIDValidator implements ConstraintValidator<IsValidUUID, String> {
#Override
public void initialize(IsValidUUID constraintAnnotation) {
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
try {
UUID.fromString(value);
return true;
} catch (Exception e) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("The provided UUID is not valid")
.addConstraintViolation();
return false;
}
}
}
You need to set the supported targets on IsValidUUID, using the following annotation.
#SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
or
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
Edit:
Sorry, I wasn't able to make it work either on a RequestParam directly. However, if you can, try creating a POJO that you can bind your request parameters to and annotate the binding field with your constraint instead. This worked for me.
public class MyModel {
#IsValidUUID
private String territoryId;
public String getTerritoryId() {
return territoryId;
}
public void setTerritoryId(String territoryId) {
this.territoryId = territoryId;
}
}
#GET
#Path("/suggestions")
#Produces(MediaType.APPLICATION_XML)
public Response getSuggestions(
#QueryParam("phrase") List<String> phrases,
#Valid #ModelAttribute MyModel myModel) {
[...]
}

Jackson deserialization errorhandling in spring-framework

I'm looking for a clean way to handle Jackson Deserialization errors for REST web requests.
More precisely: I have an Enum in a incoming DTO object, mapped from JSON. But if the user sends a wrong value, a 400 Bad Request is returned. I would like to return a 422 Unprocessable Entity with a correct message.
One option would be to accept a String, and use bean validation. However, it's not possible to pass all enum values as a list to the annotation (not a constant), so I would need to pass all enum values separately and keep them up to date. This will be very error prone over the whole application. I'm looking for a more structural way to handle this.
I solved this by using a String in the DTO and using a public #interface EnumValueas annotation.
The EnumValue:
#ReportAsSingleViolation
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EnumValueValidator.class)
#Target(ElementType.FIELD)
public #interface EnumValue {
Class<? extends Enum> value();
String message() default "The input contains validation errors.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
The validator:
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private Class<? extends Enum> enumClass;
private String message;
#Override
public void initialize(final EnumValue constraintAnnotation) {
this.enumClass = constraintAnnotation.value();
this.message = constraintAnnotation.message();
}
#Override
public boolean isValid(final String value, final ConstraintValidatorContext context) {
boolean valid = false;
for (final Enum enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
valid = true;
}
}
if (!valid) {
context.buildConstraintViolationWithTemplate(message) //
.addConstraintViolation();
}
return valid;
}
}

Custom JSR 303 validation is not invoked

My custom JSR 303 validation is not getting invoked. Here is my code
my spring config has
<mvc:annotation-driven />
My controller's handler method:
#RequestMapping(value="update", method = RequestMethod.POST ,
consumes="application/json" ,
produces="application/json"))
#ResponseBody
public String update(#Valid #RequestBody MyBean myBean){
return process(myBean);
}
MyBean (annotated with ValidMyBeanRequest):
#ValidMyBeanRequest
public class MyBean {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
ValidMyBeanRequest annotaion:
#Target({ TYPE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = {MyBeanValidator.class})
public #interface ValidMyBeanRequest {
String message() default "{validMyBeanRequest.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MyBeanValidator class:
public class MyBeanValidator implements
ConstraintValidator<ValidMyBeanRequest, MyBean> {
#Override
public void initialize(ValidMyBeanRequest constraintAnnotation) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(MyBean myBean, ConstraintValidatorContext context) {
boolean isValid = true;
int id = myBean.getId();
if(id == 0){
isValid = false;
}
return isValid;
}
}
My http POST request has below JSON data:
{id:100}
The problem is MyBeanValidator's isValid is not getting invoked. I am using Spring 3.1.0 and HibernateValidator is in classpath.
Please see what I am missing??
Update: Updated handler method to include POST request type and consumes, produces values. Also included my http request with JSON data.
Assuming that you do get model correctly, in this case you are doing everything right, except one thing: you need to handle your validation's result manually.
For achieving this you need to add BindingResult object into list of your handler parameters, and then process validation constraints in the way you would like:
#RequestMapping(value="update")
#ResponseBody
public String update(#Valid #ModelAttribute #RequestBody MyBean myBean, BindingResult result) {
if (result.hasErrors()){
return processErrors(myBean);
}
return process(myBean);
}

Resources