Resh32
Resh32

Reputation: 6590

Hibernate validation annotation - validate that at least one field is not null

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

Answers (4)

Marcus Chiu
Marcus Chiu

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

Resh32
Resh32

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

rumman0786
rumman0786

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

Slava Semushin
Slava Semushin

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

Related Questions