Ok, I did dig into validation standard (and impl) a bit, and found a problem.
IMHO the algorithm specified by JSR303 has an unintuitive mess with what (props) are recursively resolvable and what's not. I think, that's mainly due to bad distinction in grammar of annotation's properties and RB's properties.
I've made my own
MessageInterpolator, which you can find in my repo:
http://github.com/Andrey-Sisoyev/adv-msg-interpolator. It solves the problems, adds some nice caching and and also allows to address the resource bundle, where to look for the property.
Example:
Code:
@Documented
@Constraint(validatedBy = Cmp.LongCmpValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface Cmp {
String message() default "{${rb=home.lang.jsr303mod.validator.vm}Cmp.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
long value();
public enum REL { LT,LT_EQ,EQ,NEQ,GT,GT_EQ;
@Override
public String toString() {
return toString_property();
}
public String toString_property() {
switch(this) {
case LT : return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.LT}";
case LT_EQ: return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.LT_EQ}";
case EQ: return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.EQ}";
case NEQ: return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.NEQ}";
case GT : return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.GT}";
case GT_EQ: return "{${rb=home.lang.jsr303mod.validator.vm}Cmp.REL.GT_EQ}";
}
throw new UnsupportedOperationException();
}
public String toString_common() { return super.toString(); }
public String toString_math() { switch(this) {
case LT : return "<";
case LT_EQ: return "\u2264";
case EQ: return "=";
case NEQ: return "\u2260";
case GT : return ">";
case GT_EQ: return "\u2265";
}
throw new UnsupportedOperationException();
}
}
REL prop_rel_cnstr();
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@interface List {
Cmp[] value();
}
class LongCmpValidator implements ConstraintValidator<Cmp, Number> {
long cnstr_val;
REL prop_rel_cnstr;
public void initialize(Cmp constraintAnnotation) {
cnstr_val = constraintAnnotation.value();
prop_rel_cnstr = constraintAnnotation.prop_rel_cnstr();
}
public boolean isValid(Number _value, ConstraintValidatorContext context) {
if(_value == null) return true;
if( _value instanceof Integer) {
int value = _value.intValue();
switch(prop_rel_cnstr) {
case LT : return value < cnstr_val;
case LT_EQ: return value <= cnstr_val;
case EQ: return value == cnstr_val;
case NEQ: return value != cnstr_val;
case GT : return value > cnstr_val;
case GT_EQ: return value >= cnstr_val;
}
} else // ... other Number types processed
return true;
}
}
}
RB:
Code:
Cmp.REL.LT=less than
Cmp.REL.LT_EQ=less than or equal
Cmp.REL.EQ=equal
Cmp.REL.NEQ=not-equal
Cmp.REL.GT=greater
Cmp.REL.GT_EQ=greater than or equal
# will work only on advanced interpolator
Cmp.message=validated value is to be in relation "{$@prop_rel_cnstr}" to {$@value}
{${rb=home.lang.jsr303mod.validator.vm}Cmp.message} means "Cmp.message" property from bundle "home.lang.jsr303mod.validator.vm".
{$@prop_rel_cnstr} means annotation property "prop_rel_cnstr".
New resolution algorithm differs from what's specified in the JSR303 (
but is mostly backward compatible)
(1.) Resolve all the RB property recursively:
(1.1.) If RB name specified (by the property) look for it only in specified RB
(1.2.) Else look in the user's ValidationsMessages RB (or what he've configured) and if not found, look for it in the "org.hibernate.validator.ValidationMessages" inner core RB
(2.) If no more properties resolution possible, resolve annotations properties. On each resolved annotation fragment go recursively to (1).
The package is well tested (with hibernate-validator v4.1.0.Final) and ready for use, so you're welcome.