Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSR-303 Validation on Map with custom validator

I'm filling a form using Spring and Thymeleaf:

<form method="post" th:action="@{/postForm}" th:object="${myForm}"><!--/* model.addAttribute("myForm", new MyForm()) */-->
    <input type="text" th:each="id : ${idList}" th:field="*{map['__${id}__']}" /><!--/* results in map['P12345'] */-->
</form>

MyForm looks like this:

public class MyForm {
    @Quantity
    private Map<String, String> map = new HashMap<String, String>();

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }
}

As you can see I made a custom annotation @Quantity which should check if the input value(s) can be parsed as BigDecimal:

@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = QuantityValidator.class)
@Documented
public @interface Quantity {
    String message() default "{com.example.form.validation.constraints.Quantity}";
    Class<? extends Payload>[] payload() default {};
    Class<?>[] groups() default {};
}

public class QuantityValidator implements ConstraintValidator<Quantity, Map<String, String>> {
    private final DecimalFormat format = (DecimalFormat) DecimalFormat.getInstance();
    private final ParsePosition pos = new ParsePosition(0);

    @Override
    public void initialize(Quantity quantity) {
        format.setParseBigDecimal(true);
    }

    @Override
    public boolean isValid(Map<String, String> map, ConstraintValidatorContext context) {
        List<String> invalidFieldsList = new ArrayList<String>();

        for (Map.Entry<String, String> entry : map.entrySet()) {
            String quantity = entry.getValue();
            if (quantity != null && !quantity.isEmpty()) {
                if ((BigDecimal) format.parse(quantity, pos) == null) {
                    invalidFieldsList.add(entry.getKey());
                }
            }
        }

        if (!invalidFieldsList.isEmpty()) {
            context.disableDefaultConstraintViolation();

            for (String field : invalidFieldsList) {
                context.buildConstraintViolationWithTemplate("Invalid Quantity for Field: " + field).addNode(field).addConstraintViolation();
            }

            return false;
        }

        return true;
    }
}

Now in my Controller class I'm doing this:

@Controller
public class MyController {
    @RequestMapping(value = "/postForm", method = RequestMethod.POST)
    public void postForm(@ModelAttribute @Valid MyForm myForm, BindingResult bindingResult) {
        if (!bindingResult.hasErrors()) {
            // do some stuff
        }
    }
}

But getting an NotReadablePropertyException when trying to put a d into the text field for example to test the validation:

java.lang.IllegalStateException: JSR-303 validated property 'map.P12345' does not have a corresponding accessor for Spring data binding - check your DataBinder's configuration (bean property versus direct field access)
Caused by:
org.springframework.beans.NotReadablePropertyException: Invalid property 'map.P12345' of bean class [com.example.form.MyForm]: Bean property 'map.P12345' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
    at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:726) ~[spring-beans-4.1.1.RELEASE.jar:4.1.1.RELEASE]
    at org.springframework.beans.BeanWrapperImpl.getPropertyValue(BeanWrapperImpl.java:717) ~[spring-beans-4.1.1.RELEASE.jar:4.1.1.RELEASE]
    at org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:99) ~[spring-context-4.1.1.RELEASE.jar:4.1.1.RELEASE]
    at org.springframework.validation.AbstractBindingResult.getRawFieldValue(AbstractBindingResult.java:283) ~[spring-context-4.1.1.RELEASE.jar:4.1.1.RELEASE]
    at org.springframework.validation.beanvalidation.SpringValidatorAdapter.processConstraintViolations(SpringValidatorAdapter.java:143) ~[spring-context-4.1.1.RELEASE.jar:4.1.1.RELEASE]
    ... 84 more

Here's the example I read and would like to extend with a custom validator: http://viralpatel.net/blogs/spring-mvc-hashmap-form-example/

EDIT: When commenting out the @Valid annotation and checking what myForm.getMap() contains the map is filled correctly:

@Controller
public class MyController {
    private final Logger log = LogManager.getLogger(getClass());

    @RequestMapping(value = "/postForm", method = RequestMethod.POST)
    public void postForm(@ModelAttribute /*@Valid*/ MyForm myForm, BindingResult bindingResult) {
        // Output:
        // P12345: d
        // P67890: 
        for (Map.Entry<String, String> entry : myForm.getMap().entrySet()) {
            log.debug(entry.getKey() + ": " + entry.getValue());
        }
    }
}
like image 916
dtrunk Avatar asked Jan 29 '15 09:01

dtrunk


People also ask

What is the purpose of custom Validator annotation?

A custom validation annotation can also be defined at the class level to validate more than one attribute of the class. A common use case for this scenario is verifying if two fields of a class have matching values.

How do you validate a PATH variable?

Validating a PathVariable Just as with @RequestParam, we can use any annotation from the javax. validation. constraints package to validate a @PathVariable. The default message can be easily overwritten by setting the message parameter in the @Size annotation.

How do you implement a Validator?

Implementing the Validator Interface A Validator implementation must contain a constructor, a set of accessor methods for any attributes on the tag, and a validate method, which overrides the validate method of the Validator interface.


1 Answers

The ConstraintValidatorContext assumes that you are building paths to actual navigate-able properties in your object graph. Bean Validation does not actually validate this, so in theory you can add whatever, but it seems the Spring integration does use the path. Probably to map the error to the right UI element (I don't know the Spring code). What you have to do is to make sure that you add the constraints violations for the right nodes. The API actually allows for traversal of maps. Looks something like:

context.buildConstraintViolationWithTemplate( message )
        .addPropertyNode( "foo" )
        .addPropertyNode( null ).inIterable().atKey( "test" )
        .addConstraintViolation();  

'null' represents in this case the value mapped to the key. This is in contrast to adding violations to properties of the value itself which looks like:

context.buildConstraintViolationWithTemplate( message )
        .addPropertyNode( "foo" )
        .addPropertyNode( "bar" ).inIterable().atKey( "test" )
like image 84
Hardy Avatar answered Oct 20 '22 00:10

Hardy