Spring Data recursive validation - java

Does this type of validation work?
Annotation:
#Constraint(validatedBy = UniqueEmailValidator.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD})
public #interface UniqueEmail {
public String message() default "Error message";
public Class<?>[] groups() default {};
public Class<? extends Payload>[] payload() default {};
}
Validator:
#Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
#Autowired
private UserRepository userRepository;
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return userService.isEmailUnique(value); // read as a call to userRepository.findByEmail(emailAddress)
}
}
And the entity
#Entity
public class User {
...
#UniqueEmail
private String email;
}
It fails because of the recursive calls between isValid() method and userRepository.findByEmail(). It this correct behavior? Does the findByEmail always create a new User and apply the validation on it?
update:
A part of the stacktrace:
java.lang.StackOverflowError: null
...
(many times)
...
UserService.isEmailUnique(UserService.java:84)
...
UniqueEmailValidator.isValid(UniqueEmailValidator.java:29)
UniqueEmailValidator.isValid(UniqueEmailValidator.java:13)
The property spring.jpa.properties.javax.persistence.validation.mode=none resolves this. But still it was not even a double validation.

Answering on my own question.
Thanks for this hint:
In validation, you are doing a query on the same entity which means before doing the query, hibernate need to flush what is queued in your session.
In other words if I got it right, in this case hibernate saves before the validation with it's query.
So I've added the same propagation for my method:
#Transactional(propagation = Propagation.NOT_SUPPORTED)
Optional<User> findByEmail(String email);
It works.
Also I don't need to keep the spring.jpa.properties.javax.persistence.validation.mode=none anymore.

You shouldn't use find with entities just to check if an email address exists in
userService.isEmailUnique(value)
Why don't you create a method:
int countByEmail(String email)

Related

Java: custom enum validator annotation not triggered in Spring RestControllerAdvice exception handler

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);
}

JSR-303 custom annotation

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.

Validate #RequestParam to be in a list of enum

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);
}
...
}

Hibernate Error with Custom Bean Validation

I'm trying to create a custom bean validation, so I write this custom constraint:
#Documented
#Constraint(validatedBy = ValidPackageSizeValidator.class)
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidPackageSize {
String message() default "{br.com.barracuda.constraints.ValidPackageSize}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And a validator:
public class ValidPackageSizeValidator implements ConstraintValidator<ValidPackageSize, PackageSize> {
...
#Override
public boolean isValid(PackageSize value, ConstraintValidatorContext context) {
...validation login here..
}
}
Also, I wanted the validation to be performed on the service layer just after some decorators are called, so I created an another decorator to handle this task..
#Decorator
public abstract class ConstraintsViolationHandlerDecorator<T extends AbstractBaseEntity> implements CrudService<T> {
#Any
#Inject
#Delegate
CrudService<T> delegate;
#Inject
Validator validator;
#Override
#Transactional
public T save(T entity) {
triggerValidations(entity);
return delegate.save(entity);
}
private void triggerValidations(T entity) {
List<String> errorMessages = validator.validate(entity).stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
if (!errorMessages.isEmpty()) {
throw new AppConstraintViolationException(errorMessages);
}
}
}
Everything works, but if validations pass, hibernate throws an error:
ERROR [default task-6] (AssertionFailure.java:50) - HHH000099: an assertion failure occurred (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): org.hibernate.AssertionFailure: null id in br.com.barracuda.model.entities.impl.PackageSize entry (don't flush the Session after an exception occurs)
My entities use auto-generated id values.
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
Using Widlfly 9 with JEE 7.
Validation was being executed twice, once in the service layer (where I wanted it to happen) and once when entity was persisted/merged (jpa was calling it). So I disabled it by adding this line to my persistence.xml:
<property name="javax.persistence.validation.mode" value="none"/>
Now everything works fine

Annotations from javax.validation.constraints in combination with custom ConstraintValidator

I have a problem to combine the javax annotation with custom ConstraintValidators.
I have an example class Person, where the name and the age are required.
#PersonConstraint
public class Person {
#NotNull
private String name;
#NotNull
private Integer age;
...
}
And I have an additional constraint:
#Constraint(validatedBy = PersonValidator.class)
#Target(TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface PersonConstraint{
String message() default "...";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
For the custom Validator:
public class PersonValidator implements ConstraintValidator<PersonConstraint, Person> {
...
#Override
public boolean isValid(Person value, ConstraintValidatorContext context) {
if(value.getAge() < 18)
return false;
...
}
}
When a Person object is validated now and the age is null, I got a Nullpointer Exception in the PersonValidator. I thoguht, this is something, which is checked by the javax Annoation #NotNull.
Is there a solution to combine the annotations with the custom validators or do I have to check the null values in the validor by myself?
(The code is just an example - but this is a general question)
Unless you are working with the group and group sequences, there is no guaranteed order in which constraints are going to be evaluated. You cannot rely in your custom constraint that the #NotNull already has occured. And even it it had you would still get a NullPointerException. Bean Validation will per default not stop after the first constraint violation, but collect all violations of a required validation (hence a set of ConstraintViolations are returned. So there might actually be already a constraint violation for age, but in your custom ConstraintValidator you would still be accessing a null value. You cannot get around doing a null check at this stage.

Categories