Programming Thoughts
Struts 2 - Annotation-based Validation
Fixing Duplicate Parameter Names

Actually reducing Struts 2's arcane configuration

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. Struts 2 can handle duplicate parameter names but its annotation-based validation does not. This article discusses how the alternative can do what standard Struts fails to do.

Fixing Out-of-the-box

The table below summaries what goes wrong with out-of-the-box configuration and how to fix.

Failure Reason Fix
Unwanted, non-annotation conversion error messages Conversion error interceptor in the default stack activates despite the annotation Remove conversion error interceptor, which is already done
Statically created INPUT tags with the same non-index name each display entire array or collection Non-indexed name refers to entire array or collection This is dumb client code and no fix is possible
For any conversion failure, dynamically created INPUT tags with the same non-index name only display last conversion failure Every conversion failure pushes failed value onto ValueStack under non-index field name, leaving the last failure and no successful values If any conversion failures, push a fake list of all failed and successful values on to the ValueStack
Dynamically created INPUT tags with indexed field names fail when inserted into array Arrays usually not initialised large enough, so index is out of bounds and arrays can't be dynamically grown Dynamically grow list in memory and set array when all finished
Annotation conversion error messages ignored @ConversionFieldValidator does not recognise index addendum to field names @ConversionFieldValidator isn't used but AnnotationValidationInterceptor2 must recognise them

Unexpected Security Violation

The basic test for dynamically generated INPUT tags uses the following code.

<s:textfield cssClass="field" cssStyle="width: 40px;" errorPosition="bottom" name="%{'year[' + #loopStatus.index + ']'}" type="TEXT" />

The name attribute value is a forced evaluation of combining string literals with an iteration count, such as 'year[0]'. This can't quite work for the fixed page as the INPUT control submits to an indexed field named 'year[0]' but its initial value is referenced by 'form.year[0]'. This suggests the following code.

<s:set var="inputValue" value="%{'form.year[' + #loopStatus.index + ']'}" /> <s:textfield cssClass="field" cssStyle="width: 40px;" errorPosition="bottom" name="%{'year[' + #loopStatus.index + ']'}" type="TEXT" value="%{#inputValue}" />

Alas, the value is always empty string. The problem is an OGNL evaluation of a previous evaluation, a double evaluation, and these were blocked in Struts 2.5.30 as security risks. This prevents values set by a user being converted to unexpected OGNL code and executed. This can't apply here but there's no way to disable this security block. %{'form.year[0]'} would be fine as that's a single evaluation of a string literal but the OGNL expression needs to be dynamically generated.

OGNL Expression Overrides Problem Resurfaces

As there's an iterator over the form field, values can be read, such as the following, which is simpler to begin with.

<s:iterator var="yearEntry" status="loopStatus" value="form.year" >   <s:textfield cssClass="field" cssStyle="width: 40px;" errorPosition="bottom" name="%{'year[' + #loopStatus.index + ']'}" type="TEXT" value="%{#yearEntry}" />

The values are the converted values for successful conversion but default values instead of the failed value for conversion failures. What's going wrong is the iterator refers to form.year and finds the form, which does not contain conversion failures. Conversion failures are written as expression overrides named after the indexed fields, such as form.year[1]. This is a problem already encountered in Form Formatting Redesign. This requires conversion errors using indexed names to be merged into a list as well. This requires changes to the Rejected Form Values interceptor.

As the conversion failures names are just the original parameter names with the form name attached and the Annotation Validation interceptor already merges indexed names into lists, the Rejected Form Values interceptor could just be given this list. However, interceptors should be as compatible as possible with the Struts framework and that includes conversion errors using indexed names.

Combined Fields Gotcha

If fields that are combined by manual parameter conversion, such as component parts of a date, are displayed, the combined field is not accessible. This is because the component fields are defined as the formatted façade of the combined field. Further, a formatted, façade form hides the real form by using the same name. This is usually not a problem as in-page logic based on the form are based on the HTML INPUT tags the user edits, not the converted values for the form processing Action. However, the size of array or collection fields may be needed.

Consider the form of Duplicate Parameter Names Example (see next page), which has the following field and manual format function.

@ManualParameterConversion private List<Person> children = new ArrayList<>(); ... @Override public FormatResult format() { ... result = new FormatResult(); result.getFormattedMultipleFieldValues().put("childName", childNames); result.getFormattedMultipleFieldValues().put("childBirthYear", childBirthYears); return result; }

The following JSP code cannot work.

<s:property> value="form.children.size" />

Access the fake, component fields instead.

<s:property> value="form.childName.size" />

Results

Testing Example 12 and Example 13 shows how each method of multiple parameter names can be used.

Useful
INPUT tag creation Target field Indices Standard Redesigned
Hardcoded Array
Hardcoded Array Yes 1
Dynamic Array 2
Dynamic Array Yes
Hardcoded List
Hardcoded List Yes 1
Dynamic List 2
Dynamic List Yes 1

1 Except conversion annotation messages.
2 Except field messages.