New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide localized field name in JSR-303 validation messages [SPR-6407] #11073

Closed
spring-issuemaster opened this Issue Nov 22, 2009 · 2 comments

Comments

Projects
None yet
2 participants
@spring-issuemaster
Collaborator

spring-issuemaster commented Nov 22, 2009

Sebastian Beigel opened SPR-6407 and commented

I think it would be a good idea to provide the (localized) field name in JSR-303 validation messages as the first argument ("{0}"). It would be consistent with Spring's other validation (type conversion) messages and essential if you want to show a validation error summary (vs. showing the error message next to the input field in the UI, i.e. "Foo must match [a-z]+").

I have extended LocalValidatorFactoryBean and overridden validate(Object target, Errors errors) to include the field name (as a DefaultMessageSourceResolvable) as the first argument. But I think it should be included directly in LocalValidatorFactoryBean (i.e. SpringValidatorAdapter).

The code could look something like this (sorry, too lazy to make a patch :)

public void validate(Object target, Errors errors) {
    Set<ConstraintViolation<Object>> result = getValidator().validate(target);
    for (ConstraintViolation<Object> violation : result) {
        String field = violation.getPropertyPath().toString();
        FieldError fieldError = errors.getFieldError(field);
        if (fieldError == null || !fieldError.isBindingFailure()) {
            // build argument list...
            Collection<Object> args = new ArrayList<Object>();
            // ...containing the localized field name as first arg (trying the codes "model.field" and "field")...
            args.add(new DefaultMessageSourceResolvable(new String[] { errors.getObjectName() + "." + field, field }));
            // ...and JSR-303 validator's other arguments
            args.addAll(violation.getConstraintDescriptor().getAttributes().values());
            errors.rejectValue(field,
                    violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(),
                    args.toArray(),
                    violation.getMessage());
        }
    }

Resolving to messages like this:

Pattern={0} must match {4}!
model.field=Foo
field=Bar

=> Foo must match [a-z]+

Of course you could provide all the messages in the "direct" form: "Pattern.field=Field must match {4}" (w/out the patch) -- but this is cumbersome and redundant :)


Affects: 3.0 RC2

Referenced from: commits 69124f9

@spring-issuemaster

This comment has been minimized.

Collaborator

spring-issuemaster commented Nov 25, 2009

Pavla Nováková commented

Hi Sebastian,

good idea, I'm using little bit different approach to implement required behaviour, it may be helpful:

Logic implemented in my custom BeanValidator - see below (that integrates JSR-303 Validator and Spring MVC Validator) may be moved framework and the second part of this solution is to use custom format for JSR-303 ValidationMessages.properties : property value is using delimiter "-" for message key to spring message source and its arguments.

Example format of ValidationMessages.properties:

javax.validation.constraints.AssertTrue.message=constraint.assertTrue
javax.validation.constraints.DecimalMax.message=constraint.decimalMax-{value}
javax.validation.constraints.Size.message=constraint.size-{min},{max}

Then in my simple Spring message source - messages.properties

1. general messages
constraint.assertTrue=Must be true.
constraint.decimalMax=Must be lower of equal to {0}.
constraint.size=Size must be between {0} and {1}.
1. or using bean and field specific message
constraint.size.user.name=Your name length must be between {0} and {1}

Finally the implementation of my BeanValidator is:

@Component
public class BeanValidator {

    /** delimiters used in validator messages to split arguments */
    public static final String DEFAULT_ARG_DELIMITER = ",";
    public static final String MESSAGE_AND_ARGUMENTS_DELIMITER = "-";


    @Autowired private Validator validator;

    public boolean supports(Class clazz) {
        return true;
    }

    public boolean validate(Object target, Errors errors) {

        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(target);
        writeConstraintViolationsToErrors(constraintViolations, errors);
        return (constraintViolations.isEmpty());
        
    }

    public boolean validate(Object target, Errors errors, Class... groups) {

        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(target, groups);
        writeConstraintViolationsToErrors(constraintViolations, errors);
        return (constraintViolations.isEmpty());

    }


    private void writeConstraintViolationsToErrors(Set<ConstraintViolation<Object>> constraintViolations, Errors errors) {

        for (ConstraintViolation<Object> constraintViolation : constraintViolations) {

            String propertyPath = constraintViolation.getPropertyPath().toString();
            
            String rawMessage = constraintViolation.getMessage();
            Object[] args = null;
            String resolvedMessage = null;

            String[] msgAndArgs = StringUtils.split(rawMessage,MESSAGE_AND_ARGUMENTS_DELIMITER);
            Assert.isTrue(msgAndArgs.length < 3, "Badly formed validor message. Single '-' or no delimiter in message is required. Message: " + rawMessage);

            if (msgAndArgs.length == 1) resolvedMessage = rawMessage;
            else {
                resolvedMessage = msgAndArgs[0];
                args = StringUtils.split(msgAndArgs[1], DEFAULT_ARG_DELIMITER);
            }
            
            if (propertyPath != null) {
                errors.rejectValue(propertyPath, resolvedMessage, args, resolvedMessage);
                continue;
            }

            errors.reject(resolvedMessage, args, resolvedMessage);
         }

    }

    
    

This way I can use flexible Spring MVC error message resolution integrated with JSR-303 Validation.

@spring-issuemaster

This comment has been minimized.

Collaborator

spring-issuemaster commented Nov 30, 2009

Juergen Hoeller commented

Sebastian, I've implemented your suggestion for Spring 3.0 RC3 now since it really is very consistent with the established processing of bind errors in Spring.

Pavla, there are certainly alternative approaches here: We'll revisit this topic for Spring 3.1, based on the overall feedback that we'll be getting in the meantime.

Juergen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment