Spring bean validation messages resolution

[亡魂溺海] 提交于 2019-12-11 02:33:19

问题


I want to do a very specific task, get all validation messages for every field in an Object. First task is easy, getting all Annotations for field in Object, also recursively, was already done. (modified code from html5val dialect for thymeleaf)

private List<Annotation> fieldAnnotations() {
    Field field = this.fieldFinder.findField(this.targetClass, this.targetFieldName);
    if (field != null) {
        List<Annotation> annotations = Arrays.asList(field.getAnnotations());
        List<Annotation> toAdd = new ArrayList<>();
        for(Annotation a:annotations)
            if(a.annotationType().isAssignableFrom(Valid.class)){
                toAdd.addAll(new AnnotationExtractor(field.getType()).getAnnotationsForField(this.targetFieldName));
            }
            else
                toAdd.add(a);
        return toAdd;
    }
    return Collections.emptyList();
}

Now I'm trying to get message for each annotation, with internationalization.

    BeanPropertyBindingResult binding = new BeanPropertyBindingResult(realObject, root);
    for (Annotation constraint : constraints) {
        String message = AnnotationExtractor.getDefaultMessage(constraint);
        binding.rejectValue(fieldName, constraint.annotationType().getSimpleName(), message); 
    }
    List<ObjectError> errors = binding.getAllErrors();
    RequestContext requestContext = (RequestContext) arguments.getContext().getVariables().get(SpringContextVariableNames.SPRING_REQUEST_CONTEXT);
    for(ObjectError e:errors){
        String s =requestContext.getMessage(e, true);
    }

I'm getting internationalized messages, if something is resolvable by MessageSource, pretty good! Sadly, but I cannot get messages for default messages, like org.hibernate.validator.constraints.Length.message, but it's not so problematic (I always can to provide these messages by my MessageSource).

To make this task fully working, I miss one thing, arguments for messages. So, resolved message looks like this. Alias length must be between {2} and {1}. BeanPropertyBindingResult have a method to reject values with arguments, but I don't know how to get it from Annotation. It's probably done by Validator implementation, right?

It's the same job as Spring DataBinder do for invalid fields, but I want to get this messages for custom validation messages in HTML5 Form Validation. Is anyone known how to push bean through Spring internal use objects, to get these messages?

And one important thing, everything is in thymeleaf context (it's my modification of html5val dialect)


回答1:


Solution is ugly. full solution is in my fork of HTML5 Validation Dialect https://bitbucket.org/globalbus/html5-validator-dialect-fork/commits/85da11b2c13dfbd90ff05a55014d792cb453d5e2

For first, I need SpringValidatorAdapter.getArgumentsForConstraints() to be public.

public class MyValidationAdapter extends SpringValidatorAdapter{
public MyValidationAdapter(Validator targetValidator) {
    super(targetValidator);
}
@Override
public Object[] getArgumentsForConstraint(String objectName,
        String field, ConstraintDescriptor<?> descriptor) {
    return super.getArgumentsForConstraint(objectName, field, descriptor);
}
@Override
public void processConstraintViolations(
        Set<ConstraintViolation<Object>> violations, Errors errors) {
    super.processConstraintViolations(violations, errors);
}
}

We need to provide a Validator object to constructor. We can obtain it from org.thymeleaf.Arguments passed to the Dialect

this.requestContext = (RequestContext) this.arguments.getContext().getVariables().get(SpringContextVariableNames.SPRING_REQUEST_CONTEXT);

this.validatorFactory = this.requestContext.getWebApplicationContext().getBean(ValidatorFactory.class);

this.validator = new MyValidationAdapter(this.validatorFactory.getValidator());

And now, really messy code

public void processField(Element fieldElement, String fieldName){
    BeanPropertyBindingResult binding = new BeanPropertyBindingResult(
            this.realObject, this.root);
    PropertyDescriptor rootProp = this.beanDescriptor
            .getConstraintsForProperty(fieldName);
    List<PropertyDescriptor> finalProp = new LinkedList<PropertyDescriptor>();
    if (rootProp.isCascaded())// if it's nested, scan all properties for
                            // annotation
        finalProp.addAll(this.validator.getConstraintsForClass(
                rootProp.getElementClass()).getConstrainedProperties());
    else
        finalProp.add(rootProp);
    for (PropertyDescriptor prop : finalProp)
        for (final ConstraintDescriptor<?> desc : prop
                .getConstraintDescriptors()) {
            Annotation constraint = desc.getAnnotation();
            String className = this.beanDescriptor.getElementClass()
                    .getSimpleName();
            className = Character.toLowerCase(className.charAt(0))
                    + className.substring(1);
            String field = className + "." + prop.getPropertyName();
            String errorCode = constraint.annotationType().getSimpleName();
            Object[] errorArgs = this.validator.getArgumentsForConstraint(
                    this.root, field, desc);
            String message = (String) desc.getAttributes().get(ANNOTATION_MESSAGE);
            if (INTERNAL.matcher(message).find())
                message = this.validatorFactory.getMessageInterpolator().interpolate(
                        message, new Context() {

                            public Object getValidatedValue() {
                                return null;
                            }

                            public ConstraintDescriptor<?> getConstraintDescriptor() {
                                return desc;
                            }
                        });
            String[] errorCodes = binding.resolveMessageCodes(errorCode,
                    field);
            binding.addError(new FieldError(binding.getObjectName(), prop
                    .getPropertyName(), null, false, errorCodes, errorArgs,
                    message));
        }

    List<ObjectError> errors = binding.getAllErrors();
    StringBuilder customValidation = new StringBuilder();
    for (ObjectError e : errors) {
        String s = this.requestContext.getMessage(e, true);
        customValidation.append(s);
        if (!s.endsWith("."))
            customValidation.append(". ");

    }
    String onInvalid = String.format("this.setCustomValidity('%s')",
            customValidation.toString());
    fieldElement.setAttribute(ONINVALID_ATTR, onInvalid);
    fieldElement.setAttribute(ONCHANGE_ATTR, "this.setCustomValidity('')");
}

Clue is in SpringValidatorAdapter.getArgumentsForConstraints(), with valid arguments, can provide arguments for message resolution. Internal Hibernate messages can be provided by MessageInterpolator.interpolate() Final resolution was provided by RequestContext.getMessage().

This not a "clean solution", I must test it in more use cases.



来源:https://stackoverflow.com/questions/29710980/spring-bean-validation-messages-resolution

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!