Spring Boot + Thymeleaf HTML Form Handling (Part 2)
This is a continuation on the topic of HTML Form handling in the context of a Spring Boot + Thymeleaf integration.
Part #1 discussed a git Snack focused on…
- Thymeleaf markup needed to render all the standard HTML Form elements.
- Integration with the Spring Controller.
- Command Object to manage Form state.
- @ModelAttribute usage.
- Testing of the Form Controller and Command Object state.
Part #2 will look at Form Validation, particularly focusing on the use of JSR-303 (bean validation annotations).
You can chow on the Part-2 git Snack here…
Command Validation Annotations
We have trimmed down the FormCommand object from Part-1 to simplify the Validation discussion.
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
...@Data
public class FormCommand {
@Size.List({
@Size(
min = 5,
message = "{fooCommand.textField.min.message}"),
@Size(
max = 20,
message = "{fooCommand.textField.max.message}")
})
@Email(message = "{fooCommand.textField.email.message}")
String textField; @Size(min=3, max=50)
String textareaField;
}
@Size is a part of the core JSR-303 validation annotations.
import javax.validation.constraints.Size;
@Email is an extended annotation provided by the default JSR-303 implementation (Hibernate Validation).
import org.hibernate.validator.constraints.Email;
The Hibernate Validator Spec has a list of all JSR-303 core annotations as well as the list of Hibernate extended annotations…
The textField attribute is annotated with @Size(min=5, max=20) and @Email.
These annotations constrain the textField value to a minimum length of 5 and a maximum length of 20, as well as the value being a well-formed email address.
Textarea is constrained by @Size(min=3, max=50).
The textField @Size annotation is expressed as a List of 2 sub-@Size constraints. In this case it allows us to configure 2 distinct validation error messages. One message when textField violates the minimum length. Another separate specialized message is applied when the textField violates the maximum length.
The validation annotations also include an explicit message parameter.
@Email(message = "{fooCommand.textField.email.message}")
The message parameter value can contain the full literal text of the message. Better is to externalize the message text in i18n message files.
To do that use curly braces to surround a message key.
The key is then used to lookup the message text from ValidationMessages.properties.
If you navigate into the @Email annotation implementation, you will see that the default message value is “org.hibernate.validator.constraints.Email.message”.
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@ReportAsSingleViolation
@Pattern(regexp = "")
public @interface Email {
String message() default
"{org.hibernate.validator.constraints.Email.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { };
...
So you could leave out the explicit annotation message key paramter.
In that case your message for all @Email constraint violations would be the (weak) default message.
You could also add the default key into your ValidationMessages.properties and override the message.
But even if you override the default message, you will still be stuck with a single key/message that will be applied to all uses of @Email (same msg across many types of objects and error usecases).
For that reason i would recommend explicitly setting the message key when applying JSR-303 Validation annotations.
ValidationMessages.properties
The default location to look up i18n messages for JSR-303 Validation is ValidationMessages.properties.
Here is what we have there for this Snack project…
fooCommand.textField.min.message=The Text Field ${validatedValue.length() < 1 ? 'is empty': 'has only ' + validatedValue.length() + 'characters'}, but the length must be at least {min} characters.fooCommand.textField.max.message=The Text Field value has ${validatedValue.length()} characters, but the length cannot exceed {max}.fooCommand.textField.email.message=The Text Field requires an email address.
You can see the same message keys that were specified in FormCommand.
{min} and {max} are examples of parameter interpolation. When curly braces like this are encountered, then the framework first treats the inside name as a message key and tries to resolve the key from ValidationMessages.properties. If the name is not a message key, then the framework tries to find the name/value from the current context. In this case “min” and “max” are parameters of the annotation, so min=5 and max=20 are added to the current context and for the first message (fooCommand.textField.min.message), {min} is replaced with 5 (“…but the length must be at least 5 characters”…).
“validatedValue” is another well-known named-value that is added to the context. It is always the value we are currently validating.
Messages can also include JSR-341 Unified Expression Language expressions when curly brackets are prefixed with a dollar sign — ${<EL>}.
The UEL is very similar (subset) to the Spring Expression Language expressions .
The following macro uses validatedValue and a ternary If expression to vary the message depending where validatedValue is an empty String (or not).
${validatedValue.length() < 1 ?
'is empty':
'has only ' + validatedValue.length() + 'characters'}
Another useful well-known object that can be used in a macro expression is formatter, which has a format() method that behaves the same as…
java.util.Formatter.format(String format, Object… args)
Here are some more examples of message construction flexibility…
messages.properties
I have seen it suggested that you can store your Validation messages in messages.properties (rather than ValidatedMessages.properties).
Yes, technically you can do that, but the Validation messages you manage there will be under same rules/constraints as all other i18n messages.
- message parameters can be injected using curly braces (similar to ValidationMessages.properties), but the parameter name can only be numeric. And it’s often not obvious what number maps to the annotation property?
- most of the flexible formatting options available in ValidationProperties.properties are not available.
One nice feature of using messages.properties is the auto-msg-key generation. Instead of each Constraint annotation using a single default msg key, there is a decent default convention used to construct each message key that makes it more reasonable to skip explicitly configuring a message key for each use of a Constraint annotation.
For example, i did not explicitly configure a message for textareaField.
@Size(min=3, max=50)
String textareaField;
And i did not add a textareaField message to ValidationMessages.properties.
Instead i added it to messages.properties to try that option.
Size.command.textareaField=The Textarea Field must have at least {2} characters, but no mare than {1}
The key follows the convention…
<constraint-annotation-name><class-name><field-name>
Technically, <class-name> is more accurately “Model attribute key-name that maps to the class that fired the Validation error” (yikes).
That is why the msg key uses “command” (rather than “formCommand”).
The aliasing of FormCommand by “command” was done by @ModelAttribute(“command”)…
@PostMapping("/fooform")
public String foobarPost(@Valid @ModelAttribute("command") FormCommand command,
...
// leads to...model["command"] = <instance of FormCommand>
Also note that msg parameter naming is numeric (standard msg bundle parameters). No fancy Expression Language or formatting logic/flexibility either.
Size.command.textareaField=The Textarea Field must have at least {2} characters, but no mare than {1}
The default key construction when using messages.properties is nice, but not enough to offset the loss when you do not use full JSR-303 ValidationMessages.properties.
Personally i would go for the full-meal-deal offered by storing all Validation/Constraint messages in ValidationMessages.properties.
If you are really set on unifying all messages in messages.properties (or some other custom location), then i commented out some code in the main app class that can be used to configure the Validator to source JSR-303 msgs from messages.properties.
Triggering Validation
Validation can be triggered manually for fine-grained control, but the common approach is to declaratively apply a @Valid annotation to any Controller request handler parameter we wish to Validate (validation occurs before the handler body is called).
For our foobarPost() handler, the FormCommand parameter includes the @Valid annotation.
@PostMapping("/fooform")
public String foobarPost(
@Valid @ModelAttribute("command") FormCommand command,
BindingResult bindingResult,
Model model,
RedirectAttributes ra ) {
Before entering foobarPost(), the framework will test all the FormCommand Validation constraints against the “command” object parameter.
If any fail, then a ConstraintViolationException will be thrown.
By default the exception will display a 500 error page to the user. Not so frieldly.
Normally we would prefer to handle the violation in the Controller by forwarding back to the form so we can display inline User Error messaging.
To get than behavior you must immediately follow the Command parameter with a BindingResult parameter (that will prevent the exception from being thrown). I’m bolding that because it’s a bit of a weak convention. If you accidently put some other param in between, you will not get any feedback other than the ConstraintViolationException being thrown, which can be a bit confusing.
Rendering Error Messages
The project demos basic display of the Validation error messages, but there is much that can be done, particularly with css styling. For more details, check out the Thymeleaf doc…
The basic markup to display the error messages…
- Apply the attribute th:if, using the #fields helper to test whether the field you are interested in has Errors?
- If so, then apply the attribute th:errors, specifying the name of the field to extract error messages from. The default formatting is to join all error messages, each separated by <br />.
<p>Simple Text Field: <input type="text" th:field="*{textField}" /><span class="error" th:if="${#fields.hasErrors('textField')}" th:errors="*{textField}"></span></p>
Unit Testing Validation
MockMvc can be used to test that your validations are working as expected. The project includes a few happy/error cases.
Here is test that submits the Form with empty fields, expecting Validation Errors.
Overall, pretty tasty.
What i have discussed is only a few dishes from the JSR-303 buffet. For more meat, check out the spec…
http://beanvalidation.org/2.0/spec
Happy Snackin’!