In the previous algorithm, parameter values were themselves
interpolated which was undesirable and could lead to security issue.
I have reworked the algorithm to resolve the custom ResourceBundle in
priority, the built-in bundle as a back up and finally regular
parameters.
Check it out
4.3. Message interpolation
4.3.1. Default message interpolation
A conforming implementation includes a default message interpolator.
This message interpolator shall use the algorithm defined here to
interpolate message descriptors into human-readable messages.
Each constraint defines a message descriptor via its message property.
Every constraint definition shall define a default message descriptor
for that constraint. Messages can be overridden at declaration time in
constraints by setting the message property on the constraint.
The message descriptor is a string literal and may contain one or more
message parameters. Message parameters are string literals enclosed in
braces.
Example 4.1. Message using parameters
Value must be between {min} and {max}
4.3.1.1. Algorithm
The default message interpolator uses the following steps:
Message parameters are extracted from the message string and used as
keys to search the ResourceBundle named ValidationMessages (often
materialized as the property file/ValidationMessages.properties and
its locale variations) using the defined locale (see below). If a
property is found, the message parameter is replaced with the property
value in the message string. Step 1 is applied recursively until no
replacement is performed (ie. a message parameter value can itself
contain a message parameter).
Message parameters are extracted from the message string and used as
keys to search the Bean Validation provider's built-in ResourceBundle
using the defined locale (see below). If a property is found, the
message parameter is replaced with the property value in the message
string. Contrary to step 1, step 2 is not processed recursively.
If step 2 triggers a replacement, then step 1 is applied again.
Otherwise step 4 is performed.
Message parameters are extracted from the message string. Those
matching the name of an attribute of the constraint declaration are
replaced by the value of that attribute.
The defined locale is as followed:
if the locale is passed to the interpolator method interpolate(String,
CosntraintDescriptor, Object, Locale), this Locale instance is used.
otherwise, the default Locale as provided by Locale.getDefault() is
used.
The proposed algorithm ensures that custom resource bundle always have
priority over built-in resource bundle at all level of the recursive
resolution. It also ensures that constraint declarations attributes
values are not expanded further.
4.3.2. Custom message interpolation
A custom message interpolator may be provided (e.g., to interpolate
contextual data, or to adjust the default Locale used). A message
interpolator implements the MessageInterpolator interface.
/**
* Interpolate a given constraint violation message.
*
* @author Emmanuel Bernard
* @author Hardy Ferentschik
*/
public interface MessageInterpolator {
/**
* Interpolate the message from the constraint parameters and the
actual validated object.
* The locale is defaulted according to the
<code>MessageInterpolator</code> implementation
* See the implementation documentation for more detail.
*
* @param message The message to interpolate.
* @param constraintDescriptor The constraint descriptor.
* @param value The object being validated
*
* @return Interpolated error message.
*/
String interpolate(String message,
ConstraintDescriptor constraintDescriptor,
Object value);
/**
* Interpolate the message from the constraint parameters and the
actual validated object.
* The Locale used is provided as a parameter
*
* @param message The message to interpolate.
* @param constraintDescriptor The constraint descriptor.
* @param value The object being validated
* @param locale the locale targeted for the message
*
* @return Interpolated error message.
*/
String interpolate(String message,
ConstraintDescriptor constraintDescriptor,
Object value,
Locale locale);
}
message is the message descriptor as seen in
@ConstraintAnnotation.message or provided to the ConstraintContext
methods.
constraintDescriptor is the ConstraintDescriptor object representing
the metadata of the failing constraint (see Constraint metadata
request API).
value is the value being validated.
MessageInterpolator.interpolate(String, ConstraintDescriptor, Object)
is invoked by for each constraint violation report generated. The
default Locale is implementation specific.
MessageInterpolator.interpolate(String, ConstraintDescriptor, Object,
Locale) can be invoked by a wrapping MessageInterpolator to enforce a
specific Locale value by bypassing or overriding the default Locale
strategy.
A message interpolator implementation shall be threadsafe.
The message interpolator is provided to the ValidatorFactory at
construction time using
ValidatorFactoryBuilder.messageInterpolator(MessageInterpolator). This
message interpolator is shared by all validators generated by this
ValidatorFactory.
It is possible to override the MessageInterpolator implementation for
a given Validator instance by
invokingValidatorFactory
.defineValidatorState
().messageInterpolator(messageInterpolator).getValidator().
It is recommended that MessageInterpolator implementations delegate
final interpolation to the Bean Validation default MessageInterpolator
to ensure standard Bean Validation interpolation rules are followed,
The default implementation is accessible through
ValidatorFactoryBuilder.getDefaultMessageInterpolator().
4.3.3. Examples
These examples describe message interpolation based on the default
message interpolator's built-in messages (see Appendix B, Standard
ResourceBundle messages), and the ValidationMessages.propertiesfile
shown in table Table 4.2, “message interpolation”. The current locale
is assumed English.
//ValidationMessages.properties
myapp.creditcard.error=credit card number not valid
Table 4.2. message interpolation
Failing constraint declaration interpolated message
@NotNull must not be null
@Max(30) must be less than or equal to 30
@Size(min=5, max=15, message="Key must have between {min} and {max}
characters") Key must have between 5 and 15 characters
@Digits(integer=9, fraction=2) numeric value out of bounds (<9
digits>.<2 digits> expected)
@CreditCard(message={myapp.creditcard.error}) credit card number not
valid
Here is an approach to specify the Locale value to choose on a given
Validator. Locale aware MessageInterpolator. See Section 4.4,
“Bootstrapping” for more details on the APIs.
Example 4.2. Use MessageInterpolator to use a specific Locale value
/**
* delegates to a MessageInterpolator implementation but enforce a
given Locale
*/
public class LocaleSpecificMessageInterpolator implements
MessageInterpolator {
private final MessageInterpolator defaultInterpolator;
private final Locale defaultLocale;
public LocaleSpecificMessageInterpolator(MessageInterpolator
interpolator, Locale locale) {
this.defaultLocale = locale;
this.defaultInterpolator = interpolator;
}
/**
* enforece the locale passed to the interpolator
*/
public String interpolate(String message,
ConstraintDescriptor
constraintDescriptor,
Object value) {
return defaultInterpolator.interpolate(message,
constraintDescriptor,
value, this.defaultLocale);
}
// no real use, implemented for completeness
public String interpolate(String message,
ConstraintDescriptor
constraintDescriptor,
Object value,
Locale locale) {
return defaultInterpolator.interpolate(message,
constraintDescriptor, value, locale);
}
}
Locale locale = getMyCurrentLocale();
MessageInterpolator interpolator = new
LocaleSpecificMessageInterpolator(
validatorFactory.getMessageInterpolator(),
locale);
Validator validator = validatorFactory.defineValidatorState()
.messageInterpolator
(interpolator)
.getValidator();
Most of the time, however, the relevant Locale will be provided by
your application framework transparently. This framework will
implement its own version of MessageInterpolator and pass it during
theValidatorFactory configuration. The application will not have to
set the Locale itself. This example shows how a container framework
would implement MessageInterpolator to provide a user specific default
locale.
Example 4.3. Contextual container possible MessageInterpolator
implementation
public class ContextualMessageInterpolator {
private final MessageInterpolator delegate;
public ContextualMessageInterpolator(MessageInterpolator
delegate) {
this.delegate = delegate;
}
public String interpolate(String message, ConstraintDescriptor
constraintDescriptor,
Object value) {
Locale locale = Container.getManager().getUserLocale();
return this.delegate.interpolate(
message, constraintDescriptor, value, locale );
}
public String interpolate(String message, ConstraintDescriptor
constraintDescriptor,
Object value, Locale locale) {
return this.delegate.interpolate(message,
constraintDescriptor, value, locale);
}
}
//Build the ValidatorFactory
ValidatorFactoryBuilder builder = Validation.getBuilder();
ValidatorFactory factory = builder
.messageInterpolator( new
ContextualMessageInterpolator
( builder.getDefaultMessageInterpolator() ) )
.build();
//The container uses the factory to validate constraints using the
specific MessageInterpolator
Validator validator = factory.getValidator();