Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Hibernate Validator require one or another

This is an extension of my previous question. I implemented Dennis R's answer and am using hibernate-validator. Is there a way to require one field or another to be specified in the json request but not both? From my previous post, in the Request class I want the user to pass in either the id OR the code but NOT both.

I found this resource that might be the right solution for me but I don't fully understand what's going on there, why that works and frankly that looks entirely too verbose. Is that the only way to do it?

like image 609
Richard Avatar asked Apr 01 '16 20:04

Richard


2 Answers

As I commented earlier and following Nicko's answer from here, you can achieve what you want with the following code:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
public @interface FieldMatch {

    String message() default "something is wrong!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        FieldMatch[] value();
    }

    public static class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

        private String firstFieldName;
        private String secondFieldName;

        @Override
        public void initialize(FieldMatch fieldMatch) {
            firstFieldName = fieldMatch.first();
            secondFieldName = fieldMatch.second();
        }

        public boolean isValid(Object object, ConstraintValidatorContext constraintContext) {
            try {
                final Object firstObj = getProperty(object, firstFieldName);
                final Object secondObj = getProperty(object, secondFieldName);

                if(firstObj == null && secondObj == null || firstObj != null && secondObj != null) {
                    return false;
                }
            } catch (final Exception ignore) {
                // ignore
            }
            return true;
        }

        private Object getProperty(Object value, String fieldName) {
            Field[] fields = value.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.getName().equals(fieldName)) {
                    field.setAccessible(true);
                    try {
                        return field.get(value);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }
    }

}

Usage :

@FieldMatch.List({
        @FieldMatch(first = "name", second = "people"),
        @FieldMatch(first = "age", second = "abc")
})
public class Foo {

    private String name;
    private List<String> people;
    private int age;
    private Boolean abc; 
}

The only difference for you is that you don't want to check if the contents are equal, just if one field is null and the other isn't.

EDIT:

To get the object on your ExceptionHandler as asked via comments, you simply wrap the exception around a custom one and pass the object when you throw it, i.e.:

public class CustomException extends Exception {

    private String message;
    private Object model;

    public CustomException(String message, Object model) {
        super(message);
        this.model = model;
    }

    public Object getModel() {
        return model;
    }
}

With this, you can simply get it like this:

@ExceptionHandler(CustomException.class)
public ModelAndView handleCustomException(CustomException ex) {
    Object obj = ex.getModel();
    //do whatever you have to
}
like image 190
dambros Avatar answered Sep 30 '22 10:09

dambros


My goodness. The linked references look to me to be unnecessarily complex. There exists an annotation:

@org.hibernate.annotations.Check

I have often had this same case, where I want to perform exactly this type of validation, I have one field or another, or I have both or neither...

@Entity
@org.hibernate.annotations.Check(constraints = "(field1 IS NULL OR field2 IS NULL) AND (field1 IS NOT NULL OR field2 IS NOT NULL)")
public class MyEntity{
    String field1;
    Double field2;
}

This will create a check-constraint in the DB which will enforce the constraint. It shifts the validation from Hibernate and your code to the DB (which will also prevent any applications that access your DB outside of your hibernate configuration from breaking this constraint).

The creation of this annotation does not automatically execute the creation of the constraint on your database, but if/when you create the constraint, it also informs hibernate about it.

In Postgres, this constraint looks like: ALTER TABLE my_entity ADD CONSTRAINT my_entity_check CHECK ((field1 IS NULL OR field2 IS NULL) AND (field1 IS NOT NULL OR field2 IS NOT NULL));

Postgres Check Constraints

Oracle Check Constraints

If you have trouble generating the exact SQL, create your annotation, and then allow hibernate to auto-generate your DB schema against an empty database, and it will show you the correct SQL. But with the annotation, hibernate knows about the constraint as well, so can be auto-generated if you allow hibernate to generate your schema for any automated tests, etc...

like image 31
Nathan Avatar answered Sep 30 '22 09:09

Nathan