2
votes

I'm using the Hibernate @NotNull validator, and I'm trying to create custom messages to tell the user which field has generated the error when it is null. Something like this:

notNull.custom = The field {0} can't be null.

(this will be in my ValidationMessages.properties file).

Where the {0} should be the field name passed to the validator this way:

@NotNull(field="field name")

There is any way I can do that?

2
Use hibernate validator. It should have a listener to validate your all marked fields before persisting. - 00Enthusiast
I'm using Hibernate Validator... but I didn't found an option there to do this that I mentioned. 00Enthusiast, could you post some code example? - chr0x
so it should have a listener for that. For example : pre-persist event. I think you need something like @EntityListeners( JPAValidateListener.class ). Then you can handle exception if an option is invalid. Note that you should implement that listener by yourself extending hibernate built-in listener. Cannot point out exact sources, it's an idea how to accomplish it and i believe it should work. - 00Enthusiast
Ok, but the problem is not to validate the attribute... It is already being done. The problem is that now I'm needing to write specific messages for each of my fields that have @NotNull validation. The objective is have only one message and pass a attribute indicating the field name that it should be printed in the screen. I think that maybe I could use the iniatilize method from the validator class to do this task but I'm actually not sure how can I do this. - chr0x
Listener should provide you an opportunity to throw your custom message if something is wrong :) @NotNull is only a "proxy". Listener should help you to catch an exact moment for validating. - 00Enthusiast

2 Answers

3
votes

To customize your annotation message you need to disable the existing violation message inside isValid() method and build a new violation message and add it.

constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();

In the given below example, I am creating an annotation for input date validation on the basis of "invalid date", "can't be greater than today date" and "date format is correct or not".

@CheckDateIsValid(displayPattern = "DD/MM/YYYY", programPattern = "dd/MM/yyyy", groups = Order2.class)
    private String fromDate;

Annotation Interface -

public @interface CheckDateIsValid {

    String message() default "default message";

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

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

    String displayPattern();

    String programPattern();
}

Annotation implementation Class -

    public class CheckDateIsValidValidator implements ConstraintValidator<CheckDateIsValid, String> {
    @Value("${app.country.timeZone}")
    private String timeZone;
    private String displayPattern;
    private String programPattern;

    @Override
    public void initialize(CheckDateIsValid constraintAnnotation) {
        this.displayPattern = constraintAnnotation.displayPattern();
        this.programPattern = constraintAnnotation.programPattern();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        try {
            // disable existing violation message
            constraintContext.disableDefaultConstraintViolation();

            if (object == null) {
                return true;
            }

            final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(programPattern);
            LocalDateTime time = LocalDate.parse(object, formatter).atStartOfDay();

            ZoneOffset zoneOffSet = ZoneOffset.of(timeZone);
            OffsetDateTime todayDateTime = OffsetDateTime.now(zoneOffSet);

            if (time == null) {
                customMessageForValidation(constraintContext, "date is not valid");
                return false;
            } else if (todayDateTime.isBefore(time.atOffset(zoneOffSet))) {
                customMessageForValidation(constraintContext, "can't be greater than today date");
                return false;
            }

            return time != null;
        } catch (Exception e) {
            customMessageForValidation(constraintContext, "date format should be like " + displayPattern);
            return false;
        }
    }

    private void customMessageForValidation(ConstraintValidatorContext constraintContext, String message) {
        // build new violation message and add it
        constraintContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}
2
votes

If your requirement can be satisfied by interpolating hibernate messages, so you can create/name your *property file like that:

ValidationMessages.properties

And inside that:

javax.validation.constraints.NotNull.message = CUSTOMIZED MESSAGE WHEN NOTNULL is violated!

Hibernate by default searches a ResourceBundle named ValidationMessages. Also a locale might be involved: ValidationMessages_en, ValidationMessages_de, <..>

Hibernate will provide your customized message through interpolatedMessage parameter, so all ConstraintViolationException relevant information (included your message ) will be showed. So you message will be a part of real exception. Some unwieldy information will be provided!

If you want to make your custom exception (without default ConstraintViolationException behavior) check this out:

Using GenericDao concept, consider the following

  public void saveOrUpdate(IEntity<?> entity) {
      try {
          if(entity.getId == null) {
            em.persist(entity);
          } else {
            em.merge(entity)l
          }
      } catch(ConstraintViolationException cve) {
          throw new ConstraintViolationEx(constructViolationMessage(cve.getConstraintViolations()));
      }
  }

 private String constructMessage(Set<ConstraintViolation<?>> pConstraintViolations) {
    StringBuilder customMessages = new StringBuilder();
    for(ConstraintViolation<?> violation : pConstraintViolations) {
        String targetAnnotation = violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
        if(supportsCustomMessage(targetAnnotation)) {
            applyMessage(violation, targetAnnotation, customMessages);
        } else {
            // do something with not customized constraints' messages e.g. append it to existing container
        }
    }
    return customMessages.toString();
 }

 private void applyMessage(ConstraintViolation<?> pViolation, String pTargetAnnotation, StringBuilder pCustomMessages) {
     String targetClass = pViolation.getRootBean().getClass().getName();
     String targetField = pViolation.getPropertyPath().toString();
     pCustomMessages.append(MessageFormat.format(getMessageByAnnotation(pTargetAnnotation), targetClass, targetField));
     pCustomMessages.append(System.getProperty("line.separator"));
 }


 private String getBundleKey() {
     return "ValidationMessages"; //FIXME: hardcoded - implement your true key
 }

 private String getMessageByAnnotation(String pTargetAnnotation) {
     ResourceBundle messages = ResourceBundle.getBundle(getBundleKey());
     switch(pTargetAnnotation) {
     case "NotNull":
         return messages.getString(pTargetAnnotation + ".message");
     default:
         return "";
     }
 }

 private boolean supportsCustomMessage(String pTargetAnnotation) {
     return customizedConstraintsTypes.contains(pTargetAnnotation);
 }

Produced result:

test.model.exceptions.ConstraintViolationEx
    test.model.Person : name cannot be null
    test.model.Person : surname cannot be null

A hibernate ConstraintViolation provides relevant information about root class and restricted field. As you see, it applies for all hibernate supported constraints, so you need to check if current annotation can be customized by supportsCustomMessage(<..>)! If it can (it's up to you), you should get appropriate message by constraint annotation doing `getMessageByAnnotation(<..>)'.

All you need to do is implement not supported constraints logic. For example it can append it's cause message or interpolated with default message (and true exception goes to *log file)