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.