Using a custom ResourceBundle with Hibernate Validator

Robert Munteanu picture Robert Munteanu · Nov 23, 2010 · Viewed 15.1k times · Source

I'm trying to set up a custom message source for Hibernate Validator 4.1 through Spring 3.0. I've set up the necessary configuration:

<!-- JSR-303 -->
<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="validationMessageSource" ref="messageSource"/>
 </bean>

The translations are served from my message source, but it seems that the replacement tokens in the messages themselves are looked up in the message source, i.e. for a:

my.message=the property {prop} is invalid

there are calls to look up 'prop' in the messageSource. Going into ResourceBundleMessageInterpolator.interpolateMessage I note that the javadoc states:

Runs the message interpolation according to algorithm specified in JSR 303.

Note: Look-ups in user bundles is recursive whereas look-ups in default bundle are not!

This looks to me like the recursion will always take place for a user-specified bundle, so in effect I can not translate standard messages like the one for Size.

How can I plug-in my own message source and be able to have parameters be replaced in the message?

Answer

dira picture dira · Nov 23, 2010

This looks to me like the recursion will always take place for a user-specified bundle, so in effect I can not translate standard messages like the one for Size.

Hibernate Validator's ResourceBundleMessageInterpolator create two instances of ResourceBundleLocator (i.e. PlatformResourceBundleLocator) one for UserDefined validation messages - userResourceBundleLocator and the other for JSR-303 Standard validation messages - defaultResourceBundleLocator.

Any text that appears within two curly braces e.g. {someText} in the message is treated as replacementToken. ResourceBundleMessageInterpolator tries to find the matching value which can replace the replacementToken in ResourceBundleLocators.

  1. first in UserDefinedValidationMessages (which is recursive),
  2. then in DefaultValidationMessages (which is NOT recursive).

So, if you put a Standard JSR-303 message in custom ResourceBundle say, validation_erros.properties, it will be replaced by your custom message. See in this EXAMPLE Standard NotNull validation message 'may not be null' has been replaced by custom 'MyNotNullMessage' message.

How can I plug-in my own message source and be able to have parameters be replaced in the message?
my.message=the property {prop} is invalid

After going through both ResourceBundleLocators, ResourceBundleMessageInterpolator finds for more replaceTokens in the resolvedMessage (resolved by both bundles). These replacementToken are nothing but the names of Annotation's attributes, if such replaceTokens are found in the resolvedMessage, they are replaced by the values of matching Annotation attributes.

ResourceBundleMessageInterpolator.java [Line 168, 4.1.0.Final]

resolvedMessage = replaceAnnotationAttributes( resolvedMessage, annotationParameters );

Providing an example to replace {prop} with custom value, I hope it will help you....

MyNotNull.java

@Constraint(validatedBy = {MyNotNullValidator.class})
public @interface MyNotNull {
    String propertyName(); //Annotation Attribute Name
    String message() default "{myNotNull}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default {};
}

MyNotNullValidator.java

public class MyNotNullValidator implements ConstraintValidator<MyNotNull, Object> {
    public void initialize(MyNotNull parameters) {
    }

    public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
        return object != null;
    }
}

User.java

class User {
    private String userName;

    /* whatever name you provide as propertyName will replace {propertyName} in resource bundle */
   // Annotation Attribute Value 
    @MyNotNull(propertyName="userName") 
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
}

validation_errors.properties

notNull={propertyName} cannot be null 

Test

public void test() {
    LocalValidatorFactoryBean factory = applicationContext.getBean("validator", LocalValidatorFactoryBean.class);
    Validator validator = factory.getValidator();
    User user = new User("James", "Bond");
    user.setUserName(null);
    Set<ConstraintViolation<User>> violations = validator.validate(user);
    for(ConstraintViolation<User> violation : violations) {
        System.out.println("Custom Message:- " + violation.getMessage());
    }
}

Output

Custom Message:- userName cannot be null