Reputation: 6590
Is there a way to define a Hibernate validation rule using annotations as defined here, stating that at least one field shall be not null?
This would be a hypothetical example (@OneFieldMustBeNotNullConstraint
does not really exist):
@Entity
@OneFieldMustBeNotNullConstraint(list={fieldA,fieldB})
public class Card {
@Id
@GeneratedValue
private Integer card_id;
@Column(nullable = true)
private Long fieldA;
@Column(nullable = true)
private Long fieldB;
}
In the illustrated case, fieldA can be null or fieldB can be null, but not both.
One way would be to create my own validator, but I'd like to avoid if it already exists. Please share one validator if you have one already made... thanks!
Upvotes: 15
Views: 15490
Reputation: 95
If you could use javax.validation
then I would use @AssertTrue.
Using javax.validation
shouldn't be a problem since hibernate-validation
is an implementation of javax.validation
's interfaces/annotations which itself is an "implementation" of the Bean Validation 2.0 specification
@Entity
public class Card {
@Id
@GeneratedValue
private Integer card_id;
@Column(nullable = true)
private Long fieldA;
@Column(nullable = true)
private Long fieldB;
@AssertTrue(message = "at least one should be non-null")
public boolean isValid1() {
return Objects.nonNull(fieldA) || Objects.nonNull(fieldB);
}
}
Upvotes: 2
Reputation: 6590
I finally wrote the whole validator:
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import org.apache.commons.beanutils.PropertyUtils;
@Target( { TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckAtLeastOneNotNull.CheckAtLeastOneNotNullValidator.class)
@Documented
public @interface CheckAtLeastOneNotNull {
String message() default "{com.xxx.constraints.checkatleastnotnull}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] fieldNames();
public static class CheckAtLeastOneNotNullValidator implements ConstraintValidator<CheckAtLeastOneNotNull, Object> {
private String[] fieldNames;
public void initialize(CheckAtLeastOneNotNull constraintAnnotation) {
this.fieldNames = constraintAnnotation.fieldNames();
}
public boolean isValid(Object object, ConstraintValidatorContext constraintContext) {
if (object == null) {
return true;
}
try {
for (String fieldName:fieldNames){
Object property = PropertyUtils.getProperty(object, fieldName);
if (property != null) return true;
}
return false;
} catch (Exception e) {
return false;
}
}
}
}
Example of usage:
@Entity
@CheckAtLeastOneNotNull(fieldNames={"fieldA","fieldB"})
public class Reward {
@Id
@GeneratedValue
private Integer id;
private Integer fieldA;
private Integer fieldB;
[...] // accessors, other fields, etc.
}
Upvotes: 25
Reputation: 1310
This is a bit like Resh32
's answer but this will also bind the validation message with a specific field of the validated object.
The validation annotation class will be like the following one.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author rumman
* @since 9/23/19
*/
@Target(TYPE)
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = NotNullAnyValidator.class)
public @interface NotNullAny {
String[] fieldNames();
String errorOnProperty();
String messageKey() default "{error.required}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The validator class will be like the following.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Objects;
import static org.springframework.beans.BeanUtils.getPropertyDescriptor;
/**
* @author rumman
* @since 9/23/19
*/
public class NotNullAnyValidator implements ConstraintValidator<NotNullAny, Object> {
private String[] fieldNames;
private String errorOnProperty;
private String messageKey;
@Override
public void initialize(NotNullAny validateDateRange) {
fieldNames = validateDateRange.fieldNames();
errorOnProperty = validateDateRange.errorOnProperty();
messageKey = validateDateRange.messageKey();
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext validatorContext) {
Object[] fieldValues = new Object[fieldNames.length];
try {
for (int i = 0; i < fieldValues.length; i++) {
fieldValues[i] = getPropertyDescriptor(obj.getClass(), fieldNames[i]).getReadMethod().invoke(obj);
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
if (Arrays.stream(fieldValues).noneMatch(Objects::nonNull)) {
validatorContext.buildConstraintViolationWithTemplate(messageKey)
.addPropertyNode(errorOnProperty)
.addConstraintViolation()
.disableDefaultConstraintViolation();
return false;
}
return true;
}
}
Pay attention to the last if
condition block, this checks if no non null
value is found then specifies the error message, the property with which the error message will be bound to and will add the constraint violation.
To use the annotation in a class
/**
* @author rumman
* @since 9/23/19
*/
@NotNullAny(fieldNames = {"field1", "field2", "field3"},
errorOnProperty = "field1",
messageKey = "my.error.msg.key")
public class TestEntityForValidation {
private String field1;
private String field2;
private String field3;
// standard constructor(s) and getter & setters below
}
Upvotes: 1
Reputation: 15204
Just write your own validator. Is't should be pretty simple: iterate over field names and get field values by using reflection.
Concept:
Collection<String> values = Arrays.asList(
BeanUtils.getProperty(obj, fieldA),
BeanUtils.getProperty(obj, fieldB),
);
return CollectionUtils.exists(values, PredicateUtils.notNullPredicate());
There I used methods from commons-beanutils
and commons-collections
.
Upvotes: 5