Programming Thoughts
Struts 2 - Annotation-based Validation
Duplicate Parameter Names
Improving a fix to a Struts 2 feature
Struts 2 is a popular MVC framework for Java-based web applications but its annotation-based validation doesn't properly work. So, an alternative was created but it leaves multiple request parameters with the same name to manual code. Alas, Struts 2 doesn't properly work with those either. This article analyses how Struts 2 handles it.
Introduction
Previous designs have regarded multiple form values with the same name as so unusual, it can be left to manual validation. The examples below, the first uses SELECT tags with the MULTIPLE attribute, shows it can be needed.
Figure 1: Form fields that generates multiple values with the same name
Figure 2: Form fields that generates multiple values with the same name
The first example above uses fixed values, avoiding the need for validation, but the second must validate IP addresses. Struts Commons prior to v2.0.0 cannot use annotations to do this. To integrate with Struts 2 correctly, how it handles duplicate parameter names must be considered.
How standard Struts does it
If a form field is an array or collection, such as below, JSP code can loop over the values and create a form field control for each. Alas, each control has the same name, so would display the same values and field error messages. Struts gets around this if the index is appended to the name, such as 'year[0]'. This makes names unique but the Parameters interceptor recognises the indices and combines values with similar names into a list.
@CustomValidator(type="requiredEntry", message="Year is required", shortCircuit = true) @ConversionErrorFieldValidator(message = "Year must be a whole number", repopulateField = true, shortCircuit = true) public List<Integer> getYear() { return year; } public void setYear(List<Integer> year) { this.year = year; }
This can be tested in Example 12. The results are below.
| INPUT tag creation | Target field | Indices | Problems |
|---|---|---|---|
| Hardcoded | Array | If any conversion fails, first value and message applied to each field | |
| Hardcoded | Array | Yes | Older validation used, @ConversionFieldValidator ignored |
| Dynamic | Array | If any conversion fails, first value and message applied to each field | |
| Dynamic | Array | Yes | Fails to set anything |
| Hardcoded | List | If any conversion fails, first value and message applied to each field | |
| Hardcoded | List | Yes | Older validation used, @ConversionFieldValidator ignored |
| Dynamic | List | If any conversion fails, first value and message applied to each field | |
| Dynamic | List | Yes | Older validation used, @ConversionFieldValidator ignored |
What's going wrong
For not using indices, display failure when any conversion fails is not surprising as conversion errors and messages are linked to the field name and multiple INPUT tags with the same name refer to the same data and messages. For a fixed number of statically created INPUT tags, sharing the same name can never work and there is no fix. For dynamically created tags created in a loop and every conversion succeeded, iterating over the form fields works. However, if any conversion fails, it's pushed onto the ValueStack, so the iteration finds the conversion errors but that only contains the last failed value and none of the successful values. The fix is to push a fake list on the Value Stack containing all failed and successful values.
Where indices are used, the Parameters interceptor recognises them and sets individual entries of the form field but sets any conversion errors with the indexed named, such as 'year[0]'. Each INPUT tag uses its unique, indexed field name to retrieve its form field entry or conversion error. This can fail for arrays when the Parameters interceptor sets an index that is out of bounds. In the example above for fixed number of fields, the array is initialised with the correct size but the variable fields version starts with empty array. For lists, the Parameters interceptor adds null entries till the indexed value can be set. This can be fixed compiling all indexed values into a list then setting the array or collection field, not setting the field for each value in-situ.
The older validation method is a known problem and fixed by removing the ConversionError interceptor.
This leaves the silence of the @ConversionFieldValidator annotation. The ConversionErrorFieldValidator validator uses the name of the field it's annotating, missing the index addendum. That is, the validator ignores the design feature for handling duplicate parameter names.