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());
}
}
}
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.
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.
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.
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" )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With